From: Ivan Kohler Date: Tue, 13 Dec 2016 20:40:15 +0000 (-0800) Subject: Merge branch 'master' of git.freeside.biz:/home/git/freeside X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=7e4a6981a48ce6ac8dd212799f4d7e342b7db64b;hp=2bf78c35f323700c1daa3808dccab351c3fe5b14 Merge branch 'master' of git.freeside.biz:/home/git/freeside --- diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index f8b82f454..0e41b1afe 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 69717a22d..9147f5cc6 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 4821ce555..69326470a 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 35293f0ea..59792e70f 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 4e9ede303..f45abc6f8 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 35f178e25..956cf797c 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -401,19 +401,20 @@ I, I and I If I is set to a hashref with svcparts as keys and quantities as values, the appropriate FS::pkg_svc records will be replaced. I -can be set to a hashref of svcparts and flag values ('Y' or '') to set the -'hidden' field in these records. I and I 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 and I can be +set to a hashref of svcparts and flag values ('Y' or '') to set the +respective field in those records. -If I is set to the svcpart of the primary service, the appropriate -FS::pkg_svc record will be updated. +If I is set to the svcpart of the primary service, the +appropriate FS::pkg_svc record will be updated. -If I is set to a hashref, the appropriate FS::part_pkg_option records -will be replaced. +If I is set to a hashref, the appropriate FS::part_pkg_option +records will be replaced. If I 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 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 e86d0285a..51bc37f9c 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 000000000..1a0f8405e --- /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 acc32113f..8c51b35f4 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 64a75252e..84aac5bde 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 ). ''; + include('/elements/selectlayers.html', %selectlayers, 'js_only'=>1 ) . + ''; $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 5dcea09f1..50dace729 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) ); } +