summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2016-12-13 12:40:15 -0800
committerIvan Kohler <ivan@freeside.biz>2016-12-13 12:40:15 -0800
commit7e4a6981a48ce6ac8dd212799f4d7e342b7db64b (patch)
tree6b15a108896d8c50bdd82a3ee0b8575fd6c4876f
parent2bf78c35f323700c1daa3808dccab351c3fe5b14 (diff)
parent2bad20089a292ab4e9e80e8534aaec41b4bd7b2c (diff)
Merge branch 'master' of git.freeside.biz:/home/git/freeside
-rw-r--r--FS/FS/Schema.pm5
-rw-r--r--FS/FS/TaxEngine/billsoft.pm54
-rw-r--r--FS/FS/cust_main/Billing.pm21
-rw-r--r--FS/FS/cust_main/Billing_Realtime.pm30
-rw-r--r--FS/FS/cust_pkg.pm33
-rw-r--r--FS/FS/part_pkg.pm83
-rw-r--r--FS/FS/part_pkg_taxproduct.pm32
-rwxr-xr-xFS/t/suite/14-tokenization_refund.t246
-rwxr-xr-xhttemplate/browse/part_pkg.cgi8
-rwxr-xr-xhttemplate/edit/part_pkg.cgi44
-rw-r--r--httemplate/elements/tr-part_pkg-taxproducts.html4
11 files changed, 460 insertions, 100 deletions
diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm
index f8b82f4..0e41b1a 100644
--- a/FS/FS/Schema.pm
+++ b/FS/FS/Schema.pm
@@ -3248,6 +3248,7 @@ sub tables_hashref {
'adjourn_months', 'int', 'NULL', '', '', '',
'contract_end_months','int','NULL', '', '', '',
'change_to_pkgpart', 'int', 'NULL', '', '', '',
+ 'units_taxproductnum','int','NULL', '', '', '',
],
'primary_key' => 'pkgpart',
'unique' => [],
@@ -3265,6 +3266,10 @@ sub tables_hashref {
{ columns => [ 'taxproductnum' ],
table => 'part_pkg_taxproduct',
},
+ { columns => [ 'units_taxproductnum' ],
+ table => 'part_pkg_taxproduct',
+ references => [ 'taxproductnum' ],
+ },
{ columns => [ 'agentnum' ],
table => 'agent',
},
diff --git a/FS/FS/TaxEngine/billsoft.pm b/FS/FS/TaxEngine/billsoft.pm
index 69717a2..9147f5c 100644
--- a/FS/FS/TaxEngine/billsoft.pm
+++ b/FS/FS/TaxEngine/billsoft.pm
@@ -188,8 +188,7 @@ sub create_batch {
# cache some things
my (%cust_pkg, %part_pkg, %cust_location, %classname);
# keys are transaction codes (the first part of the taxproduct string)
- # and then locationnums; for per-location taxes
- my %sales;
+ my %all_tcodes;
my @options = $self->conf->config('billsoft-taxconfig');
@@ -239,8 +238,7 @@ sub create_batch {
my $taxproduct = $self->part_pkg_taxproduct($part_pkg, $classnum)
or next;
- my $tcode = substr($taxproduct, 0, 6);
- my $scode = substr($taxproduct, 6, 6);
+ my ($tcode, $scode) = split(':', $taxproduct);
# For CDRs, use the call termination site rather than setting
# Termination fields to the service address.
@@ -259,9 +257,6 @@ sub create_batch {
} # while $cdr = $cdr_search->fetch
- my $recur_tcode;
- # now write lines for the non-CDR portion of the charges
-
my $locationnum = $cust_pkg->locationnum;
# use termination address for the service location
@@ -284,13 +279,10 @@ sub create_batch {
my $taxproduct = $self->part_pkg_taxproduct($part_pkg, $_);
next unless $taxproduct;
- my $tcode = substr($taxproduct, 0, 6);
- my $scode = substr($taxproduct, 6, 6);
- $sales{$tcode} ||= 0;
- $recur_tcode = $tcode if $_ eq 'recur';
+ my ($tcode, $scode) = split(':', $taxproduct);
+ $all_tcodes{$tcode} ||= 1;
my $price = $cust_bill_pkg->get($_);
- $sales{$tcode} += $price;
$price -= $usage_total if $_ eq 'recur';
@@ -305,10 +297,9 @@ sub create_batch {
} # foreach (setup, recur)
- # S-code 21: taxes based on number of lines (E911, mostly)
- # voip_cdr and voip_inbound packages know how to report this. Not all
- # T-codes are eligible for this; only report it if the /21 taxproduct
- # exists.
+ # taxes based on number of lines (E911, mostly)
+ # mostly S-code 21 but can be others, as they want to know about
+ # Centrex trunks, PBX extensions, etc.
#
# (note: the nomenclature of "service" and "transaction" codes is
# backward from the way most people would use the terms. you'd think
@@ -318,25 +309,18 @@ sub create_batch {
# to avoid confusion.)
# XXX cache me
- # XXX this isn't precisely correct. Local exchange service on
- # high-capacity trunks, Centrex, and PBX trunks are supposed to be
- # reported as three separate implicit transactions: number of trunks,
- # of outbound channels, of extensions.
- # This is also true for VoIP PBX trunks. Come back to this.
- if ( $recur_tcode ) {
- my $lines_taxproduct = FS::part_pkg_taxproduct->count(
- 'data_vendor = \'billsoft\' and taxproduct = ?',
- sprintf('%06d%06d', $recur_tcode, 21)
- );
+ if ( my $lines_taxproduct = $part_pkg->units_taxproduct ) {
my $lines = $cust_bill_pkg->units;
-
- if ( $lines_taxproduct and $lines ) {
+ my $taxproduct = $lines_taxproduct->taxproduct;
+ my ($tcode, $scode) = split(':', $taxproduct);
+ $all_tcodes{$tcode} ||= 1;
+ if ( $lines ) {
$csv->print_hr($fh, {
%pkg_properties,
%termination,
RequestType => 'CalcTaxes',
- TransactionType => $recur_tcode,
- ServiceType => 21,
+ TransactionType => $tcode,
+ ServiceType => $scode,
Charge => 0,
Lines => $lines,
} );
@@ -345,12 +329,16 @@ sub create_batch {
} # foreach my $cust_bill_pkg
- foreach my $tcode (keys %sales) {
+ foreach my $tcode (keys %all_tcodes) {
- # S-code 43: per-invoice tax (apparently this is a thing)
+ # S-code 43: per-invoice tax
+ # XXX not exactly correct; there's "Invoice Bundle" (7:94) and
+ # "Centrex Invoice" (7:623). Local Exchange service would benefit from
+ # more high-level selection of the tax properties. (Infer from the FCC
+ # reporting options?)
my $invoice_taxproduct = FS::part_pkg_taxproduct->count(
'data_vendor = \'billsoft\' and taxproduct = ?',
- sprintf('%06d%06d', $tcode, 43)
+ $tcode . ':43'
);
if ( $invoice_taxproduct ) {
$csv->print_hr($fh, {
diff --git a/FS/FS/cust_main/Billing.pm b/FS/FS/cust_main/Billing.pm
index 4821ce5..6932647 100644
--- a/FS/FS/cust_main/Billing.pm
+++ b/FS/FS/cust_main/Billing.pm
@@ -544,14 +544,19 @@ sub bill {
foreach my $part_pkg ( @part_pkg ) {
- $cust_pkg->set($_, $hash{$_}) foreach qw ( setup last_bill bill );
+ my $this_cust_pkg = $cust_pkg;
+ # for add-on packages, copy the object to avoid leaking changes back to
+ # the caller if pkg_list is in use; see RT#73607
+ if ( $part_pkg->get('pkgpart') != $real_pkgpart ) {
+ $this_cust_pkg = FS::cust_pkg->new({ %hash });
+ }
my $pass = '';
- if ( $cust_pkg->separate_bill ) {
+ if ( $this_cust_pkg->separate_bill ) {
# if no_auto is also set, that's fine. we just need to not have
# invoices that are both auto and no_auto, and since the package
# gets an invoice all to itself, it will only be one or the other.
- $pass = $cust_pkg->pkgnum;
+ $pass = $this_cust_pkg->pkgnum;
if (!exists $cust_bill_pkg{$pass}) { # it may not exist yet
push @passes, $pass;
$total_setup{$pass} = do { my $z = 0; \$z };
@@ -565,17 +570,17 @@ sub bill {
);
$cust_bill_pkg{$pass} = [];
}
- } elsif ( ($cust_pkg->no_auto || $part_pkg->no_auto) ) {
+ } elsif ( ($this_cust_pkg->no_auto || $part_pkg->no_auto) ) {
$pass = 'no_auto';
}
- my $next_bill = $cust_pkg->getfield('bill') || 0;
+ my $next_bill = $this_cust_pkg->getfield('bill') || 0;
my $error;
# let this run once if this is the last bill upon cancellation
while ( $next_bill <= $cmp_time or $options{cancel} ) {
$error =
$self->_make_lines( 'part_pkg' => $part_pkg,
- 'cust_pkg' => $cust_pkg,
+ 'cust_pkg' => $this_cust_pkg,
'precommit_hooks' => \@precommit_hooks,
'line_items' => $cust_bill_pkg{$pass},
'setup' => $total_setup{$pass},
@@ -590,12 +595,12 @@ sub bill {
last if $error;
# or if we're not incrementing the bill date.
- last if ($cust_pkg->getfield('bill') || 0) == $next_bill;
+ last if ($this_cust_pkg->getfield('bill') || 0) == $next_bill;
# or if we're letting it run only once
last if $options{cancel};
- $next_bill = $cust_pkg->getfield('bill') || 0;
+ $next_bill = $this_cust_pkg->getfield('bill') || 0;
#stop if -o was passed to freeside-daily
last if $options{'one_recur'};
diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index 35293f0..59792e7 100644
--- a/FS/FS/cust_main/Billing_Realtime.pm
+++ b/FS/FS/cust_main/Billing_Realtime.pm
@@ -1454,9 +1454,10 @@ sub realtime_refund_bop {
( $gatewaynum, $processor, $auth, $order_number ) = ( $2, $3, $4, $6 );
}
+ my $payment_gateway;
if ( $gatewaynum ) { #gateway for the payment to be refunded
- my $payment_gateway =
+ $payment_gateway =
qsearchs('payment_gateway', { 'gatewaynum' => $gatewaynum } );
die "payment gateway $gatewaynum not found"
unless $payment_gateway;
@@ -1470,7 +1471,7 @@ sub realtime_refund_bop {
} else { #try the default gateway
my $conf_processor;
- my $payment_gateway =
+ $payment_gateway =
$self->agent->payment_gateway('method' => $options{method});
( $conf_processor, $login, $password, $namespace ) =
@@ -1487,8 +1488,27 @@ sub realtime_refund_bop {
unless ($processor eq $conf_processor)
|| (($conf_processor eq 'CardFortress') && ($processor eq $bop_options{'gateway'}));
+ $processor = $conf_processor;
+
}
+ # if gateway has switched to CardFortress but token_check hasn't run yet,
+ # tokenize just this record now, so that token gets passed/set appropriately
+ if ($cust_pay->payby eq 'CARD' && !$cust_pay->tokenized) {
+ my %tokenopts = (
+ 'payment_gateway' => $payment_gateway,
+ 'method' => 'CC',
+ 'payinfo' => $cust_pay->payinfo,
+ 'paydate' => $cust_pay->paydate,
+ );
+ my $error = $self->realtime_tokenize(\%tokenopts); # no-op unless gateway can tokenize
+ if ($self->tokenized($tokenopts{'payinfo'})) { # implies no error
+ warn " tokenizing cust_pay\n" if $DEBUG > 1;
+ $cust_pay->payinfo($tokenopts{'payinfo'});
+ $error = $cust_pay->replace;
+ }
+ return $error if $error;
+ }
} else { # didn't specify a paynum, so look for agent gateway overrides
# like a normal transaction
@@ -1567,6 +1587,12 @@ sub realtime_refund_bop {
$content{'name'} = $self->get('first'). ' '. $self->get('last');
}
}
+ if ( $cust_pay->payby eq 'CARD'
+ && !$content{'card_number'}
+ && $cust_pay->tokenized
+ ) {
+ $content{'card_token'} = $cust_pay->payinfo;
+ }
$void->content( 'action' => 'void', %content );
$void->test_transaction(1)
if $conf->exists('business-onlinepayment-test_transaction');
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index 4e9ede3..f45abc6 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -38,6 +38,8 @@ use FS::sales;
# for modify_charge
use FS::cust_credit;
+use Data::Dumper;
+
# temporary fix; remove this once (un)suspend admin notices are cleaned up
use FS::Misc qw(send_email);
@@ -3010,7 +3012,7 @@ sub modify_charge {
$pkg_opt_modified = 1;
}
}
- $pkg_opt_modified = 1 if (scalar(@old_additional) - 1) != $i;
+ $pkg_opt_modified = 1 if scalar(@old_additional) != $i;
$pkg_opt{'additional_count'} = $i if $i > 0;
my $old_classnum;
@@ -3164,9 +3166,6 @@ sub modify_charge {
'';
}
-
-
-use Data::Dumper;
sub process_bulk_cust_pkg {
my $job = shift;
my $param = shift;
@@ -5575,6 +5574,32 @@ sub _upgrade_data { # class method
my $error = $part_pkg_link->remove_linked;
die $error if $error;
}
+
+ # RT#73607: canceling a package with billing addons sometimes changes its
+ # pkgpart.
+ # Find records where the last replace_new record for the package before it
+ # was canceled has a different pkgpart from the package itself.
+ my @cust_pkg = qsearch({
+ 'table' => 'cust_pkg',
+ 'select' => 'cust_pkg.*, h_cust_pkg.pkgpart AS h_pkgpart',
+ 'addl_from' => ' JOIN (
+ SELECT pkgnum, MAX(historynum) AS historynum FROM h_cust_pkg
+ WHERE cancel IS NULL
+ AND history_action = \'replace_new\'
+ GROUP BY pkgnum
+ ) AS last_history USING (pkgnum)
+ JOIN h_cust_pkg USING (historynum)',
+ 'extra_sql' => ' WHERE cust_pkg.cancel is not null
+ AND cust_pkg.pkgpart != h_cust_pkg.pkgpart'
+ });
+ foreach my $cust_pkg ( @cust_pkg ) {
+ my $pkgnum = $cust_pkg->pkgnum;
+ warn "fixing pkgpart on canceled pkg#$pkgnum\n";
+ $cust_pkg->set('pkgpart', $cust_pkg->h_pkgpart);
+ my $error = $cust_pkg->replace;
+ die $error if $error;
+ }
+
}
=back
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 35f178e..956cf79 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -401,19 +401,20 @@ I<bulk_skip>, I<provision_hold> and I<options>
If I<pkg_svc> is set to a hashref with svcparts as keys and quantities as
values, the appropriate FS::pkg_svc records will be replaced. I<hidden_svc>
-can be set to a hashref of svcparts and flag values ('Y' or '') to set the
-'hidden' field in these records. I<bulk_skip> and I<provision_hold> can be set
-to a hashref of svcparts and flag values ('Y' or '') to set the respective field
-in those records.
+can be set to a hashref of svcparts and flag values ('Y' or '') to set the
+'hidden' field in these records. I<bulk_skip> and I<provision_hold> can be
+set to a hashref of svcparts and flag values ('Y' or '') to set the
+respective field in those records.
-If I<primary_svc> is set to the svcpart of the primary service, the appropriate
-FS::pkg_svc record will be updated.
+If I<primary_svc> is set to the svcpart of the primary service, the
+appropriate FS::pkg_svc record will be updated.
-If I<options> is set to a hashref, the appropriate FS::part_pkg_option records
-will be replaced.
+If I<options> is set to a hashref, the appropriate FS::part_pkg_option
+records will be replaced.
If I<part_pkg_currency> is set to a hashref of options (with the keys as
-option_CURRENCY), appropriate FS::part_pkg::currency records will be replaced.
+option_CURRENCY), appropriate FS::part_pkg::currency records will be
+replaced.
=cut
@@ -735,6 +736,7 @@ sub check {
|| $self->ut_floatn('pay_weight')
|| $self->ut_floatn('credit_weight')
|| $self->ut_numbern('taxproductnum')
+ || $self->ut_numbern('units_taxproductnum')
|| $self->ut_foreign_keyn('classnum', 'pkg_class', 'classnum')
|| $self->ut_foreign_keyn('addon_classnum', 'pkg_class', 'classnum')
|| $self->ut_foreign_keyn('taxproductnum',
@@ -1731,6 +1733,19 @@ sub taxproduct_description {
$part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
}
+=item units_taxproduct
+
+Returns the L<FS::part_pkg_taxproduct> record used to report the taxable
+service units (usually phone lines) on this package.
+
+=cut
+
+sub units_taxproduct {
+ my $self = shift;
+ $self->units_taxproductnum
+ ? FS::part_pkg_taxproduct->by_key($self->units_taxproductnum)
+ : '';
+}
=item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
@@ -2345,6 +2360,56 @@ sub queueable_upgrade {
die $error if $error;
}
}
+
+ # remove custom flag from one-time charge packages that were accidentally
+ # flagged as custom
+ $search = FS::Cursor->new({
+ 'table' => 'part_pkg',
+ 'hashref' => { 'freq' => '0',
+ 'custom' => 'Y',
+ 'family_pkgpart' => { op => '!=', value => '' },
+ },
+ 'addl_from' => ' JOIN
+ (select pkgpart from cust_pkg group by pkgpart having count(*) = 1)
+ AS singular_pkg USING (pkgpart)',
+ });
+ my @fields = grep { $_ ne 'pkgpart'
+ and $_ ne 'custom'
+ and $_ ne 'disabled' } FS::part_pkg->fields;
+ PKGPART: while (my $part_pkg = $search->fetch) {
+ # can't merge the package back into its parent (too late for that)
+ # but we can remove the custom flag if it's not actually customized,
+ # i.e. nothing has been changed.
+
+ my $family_pkgpart = $part_pkg->family_pkgpart;
+ next PKGPART if $family_pkgpart == $part_pkg->pkgpart;
+ my $parent_pkg = FS::part_pkg->by_key($family_pkgpart);
+ foreach my $field (@fields) {
+ if ($part_pkg->get($field) ne $parent_pkg->get($field)) {
+ next PKGPART;
+ }
+ }
+ # options have to be identical too
+ # but links, FCC options, discount plans, and usage packages can't be
+ # changed through the "modify charge" UI, so skip them
+ my %newopt = $part_pkg->options;
+ my %oldopt = $parent_pkg->options;
+ OPTION: foreach my $option (keys %newopt) {
+ if (delete $newopt{$option} ne delete $oldopt{$option}) {
+ next PKGPART;
+ }
+ }
+ if (keys(%newopt) or keys(%oldopt)) {
+ next PKGPART;
+ }
+ # okay, now replace it
+ warn "Removing custom flag from part_pkg#".$part_pkg->pkgpart."\n";
+ $part_pkg->set('custom', '');
+ my $error = $part_pkg->replace;
+ die $error if $error;
+ } # $search->fetch
+
+ return;
}
=item curuser_pkgs_sql
diff --git a/FS/FS/part_pkg_taxproduct.pm b/FS/FS/part_pkg_taxproduct.pm
index e86d028..51bc37f 100644
--- a/FS/FS/part_pkg_taxproduct.pm
+++ b/FS/FS/part_pkg_taxproduct.pm
@@ -223,7 +223,8 @@ sub batch_import {
my $imported = 0;
my $csv = Text::CSV_XS->new;
- # fields: taxproduct, description
+ my $error;
+ # for importing the "transervdesc.txt" file
while ( my $row = $csv->getline($fh) ) {
if (!defined $row) {
$dbh->rollback if $oldAutoCommit;
@@ -236,15 +237,32 @@ sub batch_import {
);
}
- my $new = FS::part_pkg_taxproduct->new({
- 'data_vendor' => 'billsoft',
- 'taxproduct' => $row->[0],
- 'description' => $row->[1],
+ # columns 0-2: irrelevant here
+ my $taxproduct = $row->[3] . ':' . $row->[5];
+ my $description = $row->[4];
+ $description =~ s/\s+$//;
+ $description .= ':' . $row->[6];
+ $description =~ s/\s+$//;
+ my $ppt = qsearchs('part_pkg_taxproduct', {
+ 'data_vendor' => 'billsoft',
+ 'taxproduct' => $taxproduct
});
- my $error = $new->insert;
+ if ( $ppt ) {
+ $ppt->set('description', $description);
+ $ppt->set('note', $row->[7]);
+ $error = $ppt->replace;
+ } else {
+ $ppt = FS::part_pkg_taxproduct->new({
+ 'data_vendor' => 'billsoft',
+ 'taxproduct' => $taxproduct,
+ 'description' => $description,
+ 'note' => $row->[7],
+ });
+ $error = $ppt->insert;
+ }
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
- return "error inserting part_pkg_taxproduct: $error\n";
+ return "error inserting part_pkg_taxproduct $taxproduct: $error\n";
}
$imported++;
}
diff --git a/FS/t/suite/14-tokenization_refund.t b/FS/t/suite/14-tokenization_refund.t
new file mode 100755
index 0000000..1a0f840
--- /dev/null
+++ b/FS/t/suite/14-tokenization_refund.t
@@ -0,0 +1,246 @@
+#!/usr/bin/perl
+
+use strict;
+use FS::Test;
+use Test::More;
+use FS::Conf;
+use FS::cust_main;
+use Business::CreditCard qw(generate_last_digit);
+use DateTime;
+if ( stat('/usr/local/etc/freeside/cardfortresstest.txt') ) {
+ plan tests => 66;
+} else {
+ plan skip_all => 'CardFortress test encryption key is not installed.';
+}
+
+#local $FS::cust_main::Billing_Realtime::DEBUG = 2;
+
+my $fs = FS::Test->new( user => 'admin' );
+my $conf = FS::Conf->new;
+my $err;
+my @bopconf;
+
+### can only run on test database (company name "Freeside Test")
+like( $conf->config('company_name'), qr/^Freeside Test/, 'using test database' ) or BAIL_OUT('');
+
+# these will just get in the way for now
+foreach my $apg ($fs->qsearch('agent_payment_gateway')) {
+ $err = $apg->delete;
+ last if $err;
+}
+ok( !$err, 'removing agent gateway overrides' ) or BAIL_OUT($err);
+
+# will need this
+my $reason = FS::reason->new_or_existing(
+ reason => 'Token Test',
+ type => 'Refund',
+ class => 'F',
+);
+isa_ok ( $reason, 'FS::reason', "refund reason" ) or BAIL_OUT('');
+
+# non-tokenizing gateway
+push @bopconf,
+'IPPay
+TESTTERMINAL';
+
+# tokenizing gateway
+push @bopconf,
+'CardFortress
+cardfortresstest
+(TEST54)
+Normal Authorization
+gateway
+IPPay
+gateway_login
+TESTTERMINAL
+gateway_password
+
+private_key
+/usr/local/etc/freeside/cardfortresstest.txt';
+
+foreach my $voiding (0,1) {
+ my $noun = $voiding ? 'void' : 'refund';
+
+ if ($voiding) {
+ $conf->delete('disable_void_after');
+ ok( !$conf->exists('disable_void_after'), 'set disable_void_after to produce voids' ) or BAIL_OUT('');
+ } else {
+ $conf->set('disable_void_after' => '0');
+ is( $conf->config('disable_void_after'), '0', 'set disable_void_after to produce refunds' ) or BAIL_OUT('');
+ }
+
+ # for attempting refund post-tokenization
+ my $n_cust_main;
+ my $n_cust_pay;
+
+ foreach my $tokenizing (0,1) {
+ my $adj = $tokenizing ? 'tokenizable' : 'non-tokenizable';
+
+ # set payment gateway
+ $conf->set('business-onlinepayment' => $bopconf[$tokenizing]);
+ is( join("\n",$conf->config('business-onlinepayment')), $bopconf[$tokenizing], "set $adj $noun default gateway" ) or BAIL_OUT('');
+
+ # make sure we're upgraded, only need to do it once,
+ # use non-tokenizing gateway for speed,
+ # but doesn't matter if existing records are tokenized or not,
+ # this suite is all about testing new record creation
+ if (!$tokenizing && !$voiding) {
+ $err = system('freeside-upgrade','-q','admin');
+ ok( !$err, 'upgrade freeside' ) or BAIL_OUT('Error string: '.$!);
+ }
+
+ if ($tokenizing) {
+
+ my $n_paynum = $n_cust_pay->paynum;
+
+ # refund the previous non-tokenized payment through CF
+ $err = $n_cust_main->realtime_refund_bop({
+ reasonnum => $reason->reasonnum,
+ paynum => $n_paynum,
+ method => 'CC',
+ });
+ ok( !$err, "run post-switch $noun" ) or BAIL_OUT($err);
+
+ my $n_cust_pay_void = $fs->qsearchs('cust_pay_void',{ paynum => $n_paynum });
+ my $n_cust_refund = $fs->qsearchs('cust_refund',{ source_paynum => $n_paynum });
+
+ if ($voiding) {
+
+ # check for void record
+ isa_ok( $n_cust_pay_void, 'FS::cust_pay_void', 'post-switch void') or BAIL_OUT("paynum $n_paynum");
+
+ # check that void tokenized
+ ok ( $n_cust_pay_void->tokenized, "post-switch void tokenized" ) or BAIL_OUT("paynum $n_paynum");
+
+ # check for no refund record
+ ok( !$n_cust_refund, "post-switch void did not generate cust_refund" ) or BAIL_OUT("paynum $n_paynum");
+
+ } else {
+
+ # check for refund record
+ isa_ok( $n_cust_refund, 'FS::cust_refund', 'post-switch refund') or BAIL_OUT("paynum $n_paynum");
+
+ # check that refund tokenized
+ ok ( $n_cust_refund->tokenized, "post-switch refund tokenized" ) or BAIL_OUT("paynum $n_paynum");
+
+ # check for no refund record
+ ok( !$n_cust_pay_void, "post-switch refund did not generate cust_pay_void" ) or BAIL_OUT("paynum $n_paynum");
+
+ }
+
+ }
+
+ # create customer
+ my $cust_main = $fs->new_customer($adj.'X'.$noun);
+ isa_ok ( $cust_main, 'FS::cust_main', "$adj $noun customer" ) or BAIL_OUT('');
+
+ # insert customer
+ $err = $cust_main->insert;
+ ok( !$err, "insert $adj $noun customer" ) or BAIL_OUT($err);
+
+ # add card
+ my $cust_payby;
+ my %card = random_card();
+ $err = $cust_main->save_cust_payby(
+ %card,
+ payment_payby => $card{'payby'},
+ auto => 1,
+ saved_cust_payby => \$cust_payby
+ );
+ ok( !$err, "save $adj $noun card" ) or BAIL_OUT($err);
+
+ # retrieve card
+ isa_ok ( $cust_payby, 'FS::cust_payby', "$adj $noun card" ) or BAIL_OUT('');
+
+ # check that card tokenized or not
+ if ($tokenizing) {
+ ok( $cust_payby->tokenized, "new $noun cust card tokenized" ) or BAIL_OUT('');
+ } else {
+ ok( !$cust_payby->tokenized, "new $noun cust card not tokenized" ) or BAIL_OUT('');
+ }
+
+ # run a payment
+ $err = $cust_main->realtime_cust_payby( amount => '1.00' );
+ ok( !$err, "run $adj $noun payment" ) or BAIL_OUT($err);
+
+ # get the payment
+ my $cust_pay = $fs->qsearchs('cust_pay',{ custnum => $cust_main->custnum });
+ isa_ok ( $cust_pay, 'FS::cust_pay', "$adj $noun payment" ) or BAIL_OUT('');
+
+ # refund the payment
+ $err = $cust_main->realtime_refund_bop({
+ reasonnum => $reason->reasonnum,
+ paynum => $cust_pay->paynum,
+ method => 'CC',
+ });
+ ok( !$err, "run $adj $noun" ) or BAIL_OUT($err);
+
+ unless ($tokenizing) {
+
+ # run a second payment, to refund after switch
+ $err = $cust_main->realtime_cust_payby( amount => '2.00' );
+ ok( !$err, "run $adj $noun second payment" ) or BAIL_OUT($err);
+
+ # get the second payment
+ $n_cust_pay = $fs->qsearchs('cust_pay',{ custnum => $cust_main->custnum, paid => '2.00' });
+ isa_ok ( $n_cust_pay, 'FS::cust_pay', "$adj $noun second payment" ) or BAIL_OUT('');
+
+ $n_cust_main = $cust_main;
+
+ }
+
+ #check that all transactions tokenized or not
+ foreach my $table (qw(cust_pay_pending cust_pay cust_pay_void cust_refund)) {
+ foreach my $record ($fs->qsearch($table,{ custnum => $cust_main->custnum })) {
+ if ($tokenizing) {
+ $err = "record not tokenized: $table ".$record->get($record->primary_key)
+ unless $record->tokenized;
+ } else {
+ $err = "record tokenized: $table ".$record->get($record->primary_key)
+ if $record->tokenized;
+ }
+ last if $err;
+ }
+ }
+ ok( !$err, "$adj transaction token check" ) or BAIL_OUT($err);
+
+ if ($voiding) {
+
+ #make sure we voided
+ ok( $fs->qsearch('cust_pay_void',{ custnum => $cust_main->custnum}), "$adj $noun record found" ) or BAIL_OUT('');
+
+ #make sure we didn't generate refund records
+ ok( !$fs->qsearch('cust_refund',{ custnum => $cust_main->custnum}), "$adj $noun did not generate cust_refund" ) or BAIL_OUT('');
+
+ } else {
+
+ #make sure we refunded
+ ok( $fs->qsearch('cust_refund',{ custnum => $cust_main->custnum}), "$adj $noun record found" ) or BAIL_OUT('');
+
+ #make sure we didn't generate void records
+ ok( !$fs->qsearch('cust_pay_void',{ custnum => $cust_main->custnum}), "$adj $noun did not generate cust_pay_void" ) or BAIL_OUT('');
+
+ }
+
+ } #end of tokenizing or not
+
+} # end of voiding or not
+
+exit;
+
+sub random_card {
+ my $payinfo = '4111' . join('', map { int(rand(10)) } 1 .. 11);
+ $payinfo .= generate_last_digit($payinfo);
+ my $paydate = DateTime->now
+ ->add('years' => 1)
+ ->truncate(to => 'month')
+ ->strftime('%F');
+ return ( 'payby' => 'CARD',
+ 'payinfo' => $payinfo,
+ 'paydate' => $paydate,
+ 'payname' => 'Tokenize Me',
+ );
+}
+
+1;
+
diff --git a/httemplate/browse/part_pkg.cgi b/httemplate/browse/part_pkg.cgi
index acc3211..8c51b35 100755
--- a/httemplate/browse/part_pkg.cgi
+++ b/httemplate/browse/part_pkg.cgi
@@ -601,12 +601,18 @@ if ( $taxclasses ) {
{ 'data' => &$taxproduct_sub($base_ppt), 'align' => 'right' },
];
}
+ if ( my $units_ppt = $part_pkg->units_taxproduct ) {
+ push @$out, [
+ { 'data' => emt('Lines'), 'align' => 'left' },
+ { 'data' => &$taxproduct_sub($units_ppt), 'align' => 'right' },
+ ];
+ }
for (my $i = 0; $i < scalar @classnums; $i++) {
my $num = $part_pkg->option('usage_taxproductnum_' . $classnums[$i]);
next if !$num;
my $ppt = FS::part_pkg_taxproduct->by_key($num);
push @$out, [
- { 'data' => $classnames[$i] . ': ', 'align' => 'left', },
+ { 'data' => $classnames[$i], 'align' => 'left', },
{ 'data' => &$taxproduct_sub($ppt), 'align' => 'right' },
];
}
diff --git a/httemplate/edit/part_pkg.cgi b/httemplate/edit/part_pkg.cgi
index 64a7525..84aac5b 100755
--- a/httemplate/edit/part_pkg.cgi
+++ b/httemplate/edit/part_pkg.cgi
@@ -40,7 +40,6 @@
'setuptax' => 'Setup fee tax exempt',
'recurtax' => 'Recurring fee tax exempt',
'taxclass' => 'Tax class',
- 'taxproduct_select'=> 'Tax products',
'plan' => 'Price plan',
'disabled' => 'Disable new orders',
'disable_line_item_date_ranges' => 'Disable line item date ranges',
@@ -73,6 +72,7 @@
'contract_end_months' => 'Contract ends after ',
'expire_months' => 'Cancel the package after ',
'change_to_pkgpart'=> 'and replace it with ',
+ 'units_taxproductnum' => 'Per-line tax product',
},
'fields' => [
@@ -214,28 +214,15 @@
type => 'hidden',
value => join(',', @taxproductnums),
},
- #{ field => 'taxproduct_select',
- # type => 'selectlayers',
- # options => [ '(default)', @taxproductnums ],
- # curr_value => '(default)',
- # labels => { ( '(default)' => '(default)' ),
- # map {($_=>$usage_class{$_})}
- # @taxproductnums
- # },
- # layer_fields => \%taxproduct_fields,
- # layer_values_callback => $taxproduct_values,
- # layers_only => !$taxproducts,
- # cell_style => ( !$taxproducts
- # ? 'display:none'
- # : ''
- # ),
- #},
{ field => 'taxproductnum',
type => 'part_pkg-taxproducts',
include_opt_callback =>
sub { pkgpart => $_[0]->pkgpart },
},
-
+ { field => 'units_taxproductnum',
+ type => ($tax_data_vendor ?
+ 'select-taxproduct' : 'hidden'),
+ },
{ type => 'tablebreak-tr-title',
value => 'Promotions', #better name?
},
@@ -445,7 +432,7 @@ my $agent_clone_extra_sql =
' ) ';
my $conf = new FS::Conf;
-my $taxproducts = $conf->config('tax_data_vendor') ne '';
+my $tax_data_vendor = $conf->config('tax_data_vendor');
my $fcc_opts = $conf->exists('part_pkg-show_fcc_options');
@@ -1112,13 +1099,8 @@ my $html_bottom = sub {
my $return =
include('/elements/selectlayers.html', %selectlayers, 'layers_only'=>1 ).
'<SCRIPT TYPE="text/javascript">'.
- include('/elements/selectlayers.html', %selectlayers, 'js_only'=>1 );
-
-# $return .=
-# "taxproduct_selectchanged(document.getElementById('taxproduct_select'));\n"
-# if $taxproducts;
-
- $return .= '</SCRIPT>';
+ include('/elements/selectlayers.html', %selectlayers, 'js_only'=>1 ) .
+ '</SCRIPT>';
$return;
@@ -1199,16 +1181,8 @@ my $field_callback = sub {
my $field = $fieldref->{field};
if ($field eq 'taxproductnums') {
$fieldref->{value} = join(',', @taxproductnums);
- } elsif ($field eq 'taxproduct_select') {
- $fieldref->{options} = [ '(default)', @taxproductnums ];
- $fieldref->{labels} = { ( '(default)' => '(default)' ),
- map {( $_ => ($usage_class{$_} || $_) )}
- @taxproductnums
- };
- $fieldref->{layer_fields} = \%taxproduct_fields;
- $fieldref->{layer_values_callback} = $taxproduct_values;
} elsif ($field eq 'taxproductnum') { # part_pkg-taxproduct, new style
- if ( !$taxproducts ) {
+ if ( !$tax_data_vendor ) {
# then make the widget go away
$fieldref->{type} = 'hidden';
}
diff --git a/httemplate/elements/tr-part_pkg-taxproducts.html b/httemplate/elements/tr-part_pkg-taxproducts.html
index 5dcea09..50dace7 100644
--- a/httemplate/elements/tr-part_pkg-taxproducts.html
+++ b/httemplate/elements/tr-part_pkg-taxproducts.html
@@ -54,7 +54,8 @@ my %pkg_options;
if ($pkgpart) {
my $part_pkg = FS::part_pkg->by_key($pkgpart);
%pkg_options = $part_pkg->options;
- $curr_values{''} = $part_pkg->taxproductnum;
+ $curr_values{''} = $cgi->param('taxproductnum')
+ || $part_pkg->taxproductnum;
}
foreach my $usage_class (@classes) {
@@ -66,4 +67,5 @@ foreach my $usage_class (@classes) {
$curr_values{$classnum} = $curr_value;
$separate = 1 if ( length($classnum) and length($curr_value) );
}
+
</%init>