agent-virtualize credit card surcharge percentage, RT#72961
[freeside.git] / FS / FS / TaxEngine.pm
index a146c54..4560142 100644 (file)
@@ -14,49 +14,67 @@ FS::TaxEngine - Base class for tax calculation engines.
 =head1 USAGE
 
 1. At the start of creating an invoice, create an FS::TaxEngine object.
 =head1 USAGE
 
 1. At the start of creating an invoice, create an FS::TaxEngine object.
-2. Each time a sale item is added to the invoice, call C<add_sale> on the 
+2. Each time a sale item is added to the invoice, call L</add_sale> on the 
    TaxEngine.
    TaxEngine.
-
-- If the TaxEngine is "batch" style (Billsoft):
 3. Set the "pending" flag on the invoice.
 4. Insert the invoice and its line items.
 3. Set the "pending" flag on the invoice.
 4. Insert the invoice and its line items.
+
+- If the TaxEngine is "batch" style (Billsoft):
 5. After creating all invoices for the day, call 
    FS::TaxEngine::process_tax_batch.  This will create the tax items for
    all of the pending invoices, clear the "pending" flag, and call 
 5. After creating all invoices for the day, call 
    FS::TaxEngine::process_tax_batch.  This will create the tax items for
    all of the pending invoices, clear the "pending" flag, and call 
-   C<collect> on each of the billed customers.
+   L<FS::cust_main::Billing/collect> on each of the billed customers.
 
 - If not (the internal tax system, CCH):
 
 - If not (the internal tax system, CCH):
-3. After adding all sale items, call C<calculate_taxes> on the TaxEngine to
+5. After adding all sale items, call L</calculate_taxes> on the TaxEngine to
    produce a list of tax line items.
    produce a list of tax line items.
-4. Append the tax line items to the invoice.
-5. Insert the invoice.
+6. Append the tax line items to the invoice.
+7. Update the invoice with the new charged amount and clear the pending flag.
 
 =head1 CLASS METHODS
 
 =over 4
 
 
 =head1 CLASS METHODS
 
 =over 4
 
+=item class
+
+Returns the class name for tax engines, according to the 'tax_data_vendor'
+configuration setting.
+
+=cut
+
+sub class {
+  my $conf = FS::Conf->new;
+  my $subclass = $conf->config('tax_data_vendor') || 'internal';
+  my $class = "FS::TaxEngine::$subclass";
+  local $@;
+  eval "use $class";
+  die "couldn't load $class: $@\n" if $@;
+
+  $class;
+}
+
 =item new 'cust_main' => CUST_MAIN, 'invoice_time' => TIME, OPTIONS...
 
 Creates an L<FS::TaxEngine> object.  The subclass will be chosen by the 
 =item new 'cust_main' => CUST_MAIN, 'invoice_time' => TIME, OPTIONS...
 
 Creates an L<FS::TaxEngine> object.  The subclass will be chosen by the 
-'enable_taxproducts' configuration setting.
+'tax_data_vendor' configuration setting.
 
 
-CUST_MAIN and TIME are required.  OPTIONS can include "cancel" => 1 to 
-indicate that the package is being billed on cancellation.
+CUST_MAIN and TIME are required.  OPTIONS can include:
+
+"cancel" => 1 to indicate that the package is being billed on cancellation.
+
+"estimate" => 1 to indicate that this calculation is for tax estimation,
+and isn't an actual sale invoice, in case that matters.
 
 =cut
 
 sub new {
   my $class = shift;
   my %opt = @_;
 
 =cut
 
 sub new {
   my $class = shift;
   my %opt = @_;
+  my $conf = FS::Conf->new;
   if ($class eq 'FS::TaxEngine') {
   if ($class eq 'FS::TaxEngine') {
-    my $conf = FS::Conf->new;
-    my $subclass = $conf->config('enable_taxproducts') || 'internal';
-    $class .= "::$subclass";
-    local $@;
-    eval "use $class";
-    die "couldn't load $class: $@\n" if $@;
+    $class = $class->class;
   }
   }
-  my $self = { items => [], taxes => {}, %opt };
+  my $self = { items => [], taxes => {}, conf => $conf, %opt };
   bless $self, $class;
 }
 
   bless $self, $class;
 }
 
@@ -84,33 +102,47 @@ Returns a hashref of metadata about this tax method, including:
 
 Adds the CUST_BILL_PKG object as a taxable sale on this invoice.
 
 
 Adds the CUST_BILL_PKG object as a taxable sale on this invoice.
 
-=item calculate_taxes CUST_BILL
+=item calculate_taxes INVOICE
 
 Calculates the taxes on the taxable sales and returns a list of 
 
 Calculates the taxes on the taxable sales and returns a list of 
-L<FS::cust_bill_pkg> objects to add to the invoice.  There is a base 
-implementation of this, which calls the C<taxline> method to calculate
-each individual tax.
+L<FS::cust_bill_pkg> objects to add to the invoice.  The base implementation
+is to call L</make_taxlines> to produce a list of "raw" tax line items, 
+then L</consolidate_taxlines> to combine those with the same itemdesc.
+
+If this fails, it will throw an exception. (Accordingly it should not trap
+exceptions from internal methods that it calls, except to translate error 
+messages into a more meaningful form.) If it succeeds, it MUST return an
+arrayref (even if the arrayref is empty).
 
 =cut
 
 sub calculate_taxes {
   my $self = shift;
 
 =cut
 
 sub calculate_taxes {
   my $self = shift;
-  my $conf = FS::Conf->new;
-
   my $cust_bill = shift;
 
   my $cust_bill = shift;
 
-  my @tax_line_items;
-  # keys are tax names (as printed on invoices / itemdesc )
-  # values are arrayrefs of taxlines
-  my %taxname;
+  my @raw_taxlines = $self->make_taxlines($cust_bill);
+  if ( !@raw_taxlines ) {
+    return;
+  } elsif ( !ref $raw_taxlines[0] ) { # error message
+    return $raw_taxlines[0];
+  }
 
 
-  # keys are taxnums
-  # values are (cumulative) amounts
-  my %tax_amount;
+  my @real_taxlines = $self->consolidate_taxlines(@raw_taxlines);
 
 
-  # keys are taxnums
-  # values are arrayrefs of cust_tax_exempt_pkg objects
-  my %tax_exemption;
+  if ( $cust_bill and $cust_bill->get('invnum') ) {
+    $_->set('invnum', $cust_bill->get('invnum')) foreach @real_taxlines;
+  }
+  return \@real_taxlines;
+}
+
+sub make_taxlines {
+  # only used by FS::TaxEngine::internal; should just move there
+  my $self = shift;
+  my $conf = $self->{conf};
+
+  my $cust_bill = shift;
+
+  my @raw_taxlines;
 
   # For each distinct tax rate definition, calculate the tax and exemptions.
   foreach my $taxnum ( keys %{ $self->{taxes} } ) {
 
   # For each distinct tax rate definition, calculate the tax and exemptions.
   foreach my $taxnum ( keys %{ $self->{taxes} } ) {
@@ -122,43 +154,86 @@ sub calculate_taxes {
     # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
     # (setup, recurring, usage classes)
 
     # the rest of @{ $taxlisthash->{$tax} } is cust_bill_pkg component objects
     # (setup, recurring, usage classes)
 
-    my $taxline = $self->taxline('tax' => $tax_object, 'sales' => $taxables);
-    # taxline methods are now required to return real line items
-    # with their link records
-    die $taxline unless ref($taxline);
+    my @taxlines = $self->taxline('tax' => $tax_object, 'sales' => $taxables);
+    # taxline methods are now required to return the link records alone.
+    # Consolidation will take care of the rest.
+    next if !@taxlines;
+    die $taxlines[0] unless ref($taxlines[0]);
 
 
-    push @{ $taxname{ $taxline->itemdesc } }, $taxline;
+    push @raw_taxlines, @taxlines;
 
   } #foreach $taxnum
 
 
   } #foreach $taxnum
 
-  my $link_table = $self->info->{link_table};
-  # For each distinct tax name (the values set as $taxline->itemdesc),
-  # create a consolidated tax item with the total amount and all the links
-  # of all tax items that share that name.
-  foreach my $taxname ( keys %taxname ) {
-    my @tax_links;
-    my $tax_cust_bill_pkg = FS::cust_bill_pkg->new({
-        'invnum'    => $cust_bill->invnum,
+  return @raw_taxlines;
+}
+
+sub consolidate_taxlines {
+
+  my $self = shift;
+  my $conf = $self->{conf};
+
+  my @raw_taxlines = @_;
+  return if !@raw_taxlines; # shouldn't even be here
+
+  my @tax_line_items;
+
+  # keys are tax names (as printed on invoices / itemdesc )
+  # values are arrayrefs of tax links ("raw taxlines")
+  my %taxname;
+  # collate these by itemdesc
+  foreach my $taxline (@raw_taxlines) {
+    my $taxname = $taxline->taxname;
+    $taxname{$taxname} ||= [];
+    push @{ $taxname{$taxname} }, $taxline;
+  }
+
+  # keys are taxnums
+  # values are (cumulative) amounts
+  my %tax_amount;
+
+  my $link_table = $raw_taxlines[0]->table;
+
+  # Preconstruct cust_bill_pkg objects that will become the "final"
+  # taxlines for each name, so that we can reference them.
+  # (keys are taxnames)
+  my %real_taxline_named = map {
+    $_ => FS::cust_bill_pkg->new({
         'pkgnum'    => 0,
         'recur'     => 0,
         'sdate'     => '',
         'edate'     => '',
         'pkgnum'    => 0,
         'recur'     => 0,
         'sdate'     => '',
         'edate'     => '',
-        'itemdesc'  => $taxname,
-        $link_table => \@tax_links,
-    });
+        'itemdesc'  => $_
+    })
+  } keys %taxname;
+
+  # For each distinct tax name (the values set as $taxline->itemdesc),
+  # create a consolidated tax item with the total amount and all the links
+  # of all tax items that share that name.
+  foreach my $taxname ( keys %taxname ) {
+    my $tax_links = $taxname{$taxname};
+    my $tax_cust_bill_pkg = $real_taxline_named{$taxname};
+    $tax_cust_bill_pkg->set( $link_table => $tax_links );
 
     my $tax_total = 0;
     warn "adding $taxname\n" if $DEBUG > 1;
 
 
     my $tax_total = 0;
     warn "adding $taxname\n" if $DEBUG > 1;
 
-    foreach my $taxitem ( @{ $taxname{$taxname} } ) {
+    foreach my $link ( @$tax_links ) {
       # then we need to transfer the amount and the links from the
       # line item to the new one we're creating.
       # then we need to transfer the amount and the links from the
       # line item to the new one we're creating.
-      $tax_total += $taxitem->setup;
-      foreach my $link ( @{ $taxitem->get($link_table) } ) {
-        $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
-        push @tax_links, $link;
+      $tax_total += $link->amount;
+      $link->set('tax_cust_bill_pkg', $tax_cust_bill_pkg);
+
+      # if the link represents tax on tax, also fix its taxable pointer
+      # to point to the "final" taxline
+      my $taxable_cust_bill_pkg = $link->get('taxable_cust_bill_pkg');
+      if ( $taxable_cust_bill_pkg and
+           my $other_taxname = $taxable_cust_bill_pkg->itemdesc) {
+        $link->set('taxable_cust_bill_pkg',
+          $real_taxline_named{$other_taxname}
+        );
       }
       }
-    } # foreach $taxitem
+
+    } # foreach $link
     next unless $tax_total;
 
     # we should really neverround this up...I guess it's okay if taxline 
     next unless $tax_total;
 
     # we should really neverround this up...I guess it's okay if taxline 
@@ -185,7 +260,7 @@ sub calculate_taxes {
     push @tax_line_items, $tax_cust_bill_pkg;
   }
 
     push @tax_line_items, $tax_cust_bill_pkg;
   }
 
-  \@tax_line_items;
+  @tax_line_items;
 }
 
 =head1 CLASS METHODS
 }
 
 =head1 CLASS METHODS