summaryrefslogtreecommitdiff
path: root/FS
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2016-11-01 16:20:44 -0700
committerIvan Kohler <ivan@freeside.biz>2016-11-01 16:20:44 -0700
commitf2b43a877c70aa367595fe2fc4fcffd82f62d001 (patch)
tree83c4be349cd80ec6fefa9684b390c8989d550598 /FS
parentbd29d65b7ec7b2637656fbc66ae0f57fa02dcbce (diff)
parent6a4b5b3bf9b3e589cf8ff18453e9c6be6a50091a (diff)
Merge branch 'master' of git.freeside.biz:/home/git/freeside
Diffstat (limited to 'FS')
-rw-r--r--FS/FS/ClientAPI/MyAccount.pm2
-rw-r--r--FS/FS/cust_main.pm2
-rw-r--r--FS/FS/cust_main/Billing_Realtime.pm58
-rw-r--r--FS/FS/cust_main_county.pm80
-rw-r--r--FS/FS/cust_payby.pm6
-rw-r--r--FS/FS/geocode_Mixin.pm2
-rw-r--r--FS/FS/part_pkg.pm20
-rw-r--r--FS/FS/part_pkg/global_Mixin.pm11
-rw-r--r--FS/FS/part_pkg/voip_cdr.pm25
-rw-r--r--FS/FS/part_pkg/voip_inbound.pm10
-rw-r--r--FS/FS/payinfo_Mixin.pm18
11 files changed, 142 insertions, 92 deletions
diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm
index 7c17ae39e..091d6ac68 100644
--- a/FS/FS/ClientAPI/MyAccount.pm
+++ b/FS/FS/ClientAPI/MyAccount.pm
@@ -1022,7 +1022,7 @@ sub validate_payment {
validate($payinfo)
or return { 'error' => gettext('invalid_card') }; # . ": ". $self->payinfo
return { 'error' => gettext('unknown_card_type') }
- if $payinfo !~ /^99\d{14}$/ && cardtype($payinfo) eq "Unknown";
+ if !$cust_main->tokenized($payinfo) && cardtype($payinfo) eq "Unknown";
if ( length($p->{'paycvv'}) && $p->{'paycvv'} !~ /^\s*$/ ) {
if ( cardtype($payinfo) eq 'American Express card' ) {
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 9c8e37499..2136ad285 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -1895,7 +1895,7 @@ sub check_payinfo_cardtype {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
- return '' if $payinfo =~ /^99\d{14}$/; #token
+ return '' if $self->tokenized($payinfo); #token
my %bop_card_types = map { $_=>1 } values %{ card_types() };
my $cardtype = cardtype($payinfo);
diff --git a/FS/FS/cust_main/Billing_Realtime.pm b/FS/FS/cust_main/Billing_Realtime.pm
index 0b6b09981..7718f7aab 100644
--- a/FS/FS/cust_main/Billing_Realtime.pm
+++ b/FS/FS/cust_main/Billing_Realtime.pm
@@ -376,14 +376,18 @@ sub _bop_content {
}
sub _tokenize_card {
- my ($self,$transaction,$cust_payby,$log,%opt) = @_;
+ my ($self,$transaction,$options,$log,%opt) = @_;
+ # options is for entire process, so we can update payinfo
+ # opt is just for this call, only key is replace
+ my $cust_payby = $options->{'cust_payby'};
if ( $cust_payby
and $transaction->can('card_token')
and $transaction->card_token
- and $cust_payby->payinfo !~ /^99\d{14}$/ #not already tokenized
+ and !$cust_payby->tokenized #not already tokenized
) {
+ $options->{'payinfo'} = $transaction->card_token;
$cust_payby->payinfo($transaction->card_token);
my $error;
@@ -400,6 +404,18 @@ sub _tokenize_card {
}
+# only store payinfo in cust_pay/cust_pay_pending
+# if it's a tokenized card or if processor requires card for void
+sub _cust_pay_opts {
+ my ($self,$payby,$payinfo,$transaction) = @_;
+ ( (($payby eq 'CARD') && $self->tokenized($payinfo))
+ || (($payby eq 'CARD') && $transaction->info('CC_void_requires_card'))
+ || (($payby eq 'CHEK') && $transaction->info('ECHECK_void_requires_account'))
+ )
+ ? ('payinfo' => $payinfo)
+ : ();
+}
+
my %bop_method2payby = (
'CC' => 'CARD',
'ECHECK' => 'CHEK',
@@ -665,12 +681,15 @@ sub realtime_bop {
#okay, good to go, if we're a duplicate, cust_pay_pending will kick us out
+ my $transaction = new $namespace( $payment_gateway->gateway_module,
+ $self->_bop_options(\%options),
+ );
+
my $cust_pay_pending = new FS::cust_pay_pending {
'custnum' => $self->custnum,
'paid' => $options{amount},
'_date' => '',
'payby' => $bop_method2payby{$options{method}},
- 'payinfo' => $options{payinfo},
'paymask' => $options{paymask},
'paydate' => $paydate,
'recurring_billing' => $content{recurring_billing},
@@ -679,6 +698,7 @@ sub realtime_bop {
'gatewaynum' => $payment_gateway->gatewaynum || '',
'session_id' => $options{session_id} || '',
'jobnum' => $options{depend_jobnum} || '',
+ $self->_cust_pay_opts($options{payinfo},$transaction),
};
$cust_pay_pending->payunique( $options{payunique} )
if defined($options{payunique}) && length($options{payunique});
@@ -695,10 +715,6 @@ sub realtime_bop {
my( $action1, $action2 ) =
split( /\s*\,\s*/, $payment_gateway->gateway_action );
- my $transaction = new $namespace( $payment_gateway->gateway_module,
- $self->_bop_options(\%options),
- );
-
$transaction->content(
'type' => $options{method},
$self->_bop_auth(\%options),
@@ -811,7 +827,7 @@ sub realtime_bop {
# Tokenize
###
- my $error = $self->_tokenize_card($transaction,$options{'cust_payby'},$log,'replace' => 1);
+ my $error = $self->_tokenize_card($transaction,\%options,$log,'replace' => 1);
return $error if $error;
###
@@ -849,9 +865,7 @@ sub fake_bop {
'paid' => $options{amount},
'_date' => '',
'payby' => $bop_method2payby{$options{method}},
- #'payinfo' => $payinfo,
'payinfo' => '4111111111111111',
- #'paydate' => $paydate,
'paydate' => '2012-05-01',
'processor' => 'FakeProcessor',
'auth' => '54',
@@ -925,7 +939,6 @@ sub _realtime_bop_result {
'paid' => $cust_pay_pending->paid,
'_date' => '',
'payby' => $cust_pay_pending->payby,
- 'payinfo' => $options{'payinfo'},
'paymask' => $options{'paymask'} || $cust_pay_pending->paymask,
'paydate' => $cust_pay_pending->paydate,
'pkgnum' => $cust_pay_pending->pkgnum,
@@ -935,6 +948,7 @@ sub _realtime_bop_result {
'auth' => $transaction->authorization,
'order_number' => $order_number || '',
'no_auto_apply' => $options{'no_auto_apply'} ? 'Y' : '',
+ $self->_cust_pay_opts($options{payinfo},$transaction),
} );
#doesn't hurt to know, even though the dup check is in cust_pay_pending now
$cust_pay->payunique( $options{payunique} )
@@ -1840,7 +1854,9 @@ sub realtime_verify_bop {
###
my $error;
- my $transaction; #need this back so we can do _tokenize_card
+ my $transaction = new $namespace( $payment_gateway->gateway_module,
+ $self->_bop_options(\%options),
+ ); #need this back so we can do _tokenize_card
# don't mutex the customer here, because they might be uncommitted. and
# this is only verification. it doesn't matter if they have other
@@ -1851,13 +1867,13 @@ sub realtime_verify_bop {
'paid' => '1.00',
'_date' => '',
'payby' => $bop_method2payby{'CC'},
- 'payinfo' => $options{payinfo},
'paymask' => $options{paymask},
'paydate' => $paydate,
'pkgnum' => $options{'pkgnum'},
'status' => 'new',
'gatewaynum' => $payment_gateway->gatewaynum || '',
'session_id' => $options{session_id} || '',
+ $self->_cust_pay_opts($options{payinfo},$transaction),
};
$cust_pay_pending->payunique( $options{payunique} )
if defined($options{payunique}) && length($options{payunique});
@@ -1888,10 +1904,6 @@ sub realtime_verify_bop {
if $DEBUG > 1;
warn Dumper($cust_pay_pending) if $DEBUG > 2;
- $transaction = new $namespace( $payment_gateway->gateway_module,
- $self->_bop_options(\%options),
- );
-
$transaction->content(
'type' => 'CC',
$self->_bop_auth(\%options),
@@ -2115,7 +2127,7 @@ sub realtime_verify_bop {
#important that we not pass replace option here,
#because cust_payby->replace uses realtime_verify_bop!
- $self->_tokenize_card($transaction,$options{'cust_payby'},$log);
+ $self->_tokenize_card($transaction,\%options,$log);
###
# result handling
@@ -2162,7 +2174,7 @@ sub realtime_tokenize {
return "No cust_payby" unless $options{'cust_payby'};
$self->_bop_cust_payby_options(\%options);
return '' unless $options{method} eq 'CC';
- return '' if $options{payinfo} =~ /^99\d{14}$/; #already tokenized
+ return '' if $self->tokenized($options{payinfo}); #already tokenized
###
# select a gateway
@@ -2254,7 +2266,7 @@ sub realtime_tokenize {
#important that we not pass replace option here,
#because cust_payby->replace uses realtime_tokenize!
- $self->_tokenize_card($transaction,$options{'cust_payby'},$log);
+ $self->_tokenize_card($transaction,\%options,$log);
} else {
@@ -2266,6 +2278,12 @@ sub realtime_tokenize {
}
+sub tokenized {
+ my $this = shift;
+ my $payinfo = shift;
+ $payinfo =~ /^99\d{14}$/;
+}
+
=back
=head1 BUGS
diff --git a/FS/FS/cust_main_county.pm b/FS/FS/cust_main_county.pm
index a1233d083..65fb72208 100644
--- a/FS/FS/cust_main_county.pm
+++ b/FS/FS/cust_main_county.pm
@@ -682,6 +682,37 @@ END
}
+sub _merge_into {
+ # for internal use: takes another cust_main_county object, transfers
+ # all existing references to this record to that one, and deletes this
+ # one.
+ my $record = shift;
+ my $other = shift or die "record to merge into must be provided";
+ my $new_taxnum = $other->taxnum;
+ my $old_taxnum = $record->taxnum;
+ if ($other->tax != $record->tax or
+ $other->exempt_amount != $record->exempt_amount) {
+ # don't assume these are the same.
+ warn "Found duplicate taxes (#$new_taxnum and #$old_taxnum) but they have different rates and can't be merged.\n";
+ } else {
+ warn "Merging tax #$old_taxnum into #$new_taxnum\n";
+ foreach my $table (qw(
+ cust_bill_pkg_tax_location
+ cust_bill_pkg_tax_location_void
+ cust_tax_exempt_pkg
+ cust_tax_exempt_pkg_void
+ )) {
+ foreach my $row (qsearch($table, { 'taxnum' => $old_taxnum })) {
+ $row->set('taxnum' => $new_taxnum);
+ my $error = $row->replace;
+ die $error if $error;
+ }
+ }
+ my $error = $record->delete;
+ die $error if $error;
+ }
+}
+
sub _upgrade_data {
my $class = shift;
# assume taxes in Washington with district numbers, and null name, or
@@ -704,6 +735,28 @@ sub _upgrade_data {
}
FS::upgrade_journal->set_done($journal);
}
+ my @key_fields = (qw(city county state country district taxname taxclass));
+
+ # remove duplicates (except disabled records)
+ my @duplicate_sets = qsearch({
+ table => 'cust_main_county',
+ select => FS::Record::group_concat_sql('taxnum', ',') . ' AS taxnums, ' .
+ join(',', @key_fields),
+ extra_sql => ' WHERE tax > 0
+ GROUP BY city, county, state, country, district, taxname, taxclass
+ HAVING COUNT(*) > 1'
+ });
+ warn "Found ".scalar(@duplicate_sets)." set(s) of duplicate tax definitions\n"
+ if @duplicate_sets;
+ foreach my $set (@duplicate_sets) {
+ my @taxnums = split(',', $set->get('taxnums'));
+ my $first = FS::cust_main_county->by_key(shift @taxnums);
+ foreach my $taxnum (@taxnums) {
+ my $record = FS::cust_main_county->by_key($taxnum);
+ $record->_merge_into($first);
+ }
+ }
+
# trim whitespace and convert to uppercase in the 'city' field.
foreach my $record (qsearch({
table => 'cust_main_county',
@@ -714,33 +767,10 @@ sub _upgrade_data {
# create an exact duplicate.
# so find the record this one would duplicate, and merge them.
$record->check; # trims whitespace
- my %match = map { $_ => $record->get($_) }
- qw(city county state country district taxname taxclass);
+ my %match = map { $_ => $record->get($_) } @key_fields;
my $other = qsearchs('cust_main_county', \%match);
if ($other) {
- my $new_taxnum = $other->taxnum;
- my $old_taxnum = $record->taxnum;
- if ($other->tax != $record->tax or
- $other->exempt_amount != $record->exempt_amount) {
- # don't assume these are the same.
- warn "Found duplicate taxes (#$new_taxnum and #$old_taxnum) but they have different rates and can't be merged.\n";
- } else {
- warn "Merging tax #$old_taxnum into #$new_taxnum\n";
- foreach my $table (qw(
- cust_bill_pkg_tax_location
- cust_bill_pkg_tax_location_void
- cust_tax_exempt_pkg
- cust_tax_exempt_pkg_void
- )) {
- foreach my $row (qsearch($table, { 'taxnum' => $old_taxnum })) {
- $row->set('taxnum' => $new_taxnum);
- my $error = $row->replace;
- die $error if $error;
- }
- }
- my $error = $record->delete;
- die $error if $error;
- }
+ $record->_merge_into($other);
} else {
# else there is no record this one duplicates, so just fix it
my $error = $record->replace;
diff --git a/FS/FS/cust_payby.pm b/FS/FS/cust_payby.pm
index 626fc9fe9..53608cf64 100644
--- a/FS/FS/cust_payby.pm
+++ b/FS/FS/cust_payby.pm
@@ -276,7 +276,7 @@ sub replace {
if ( $self->payby =~ /^(CARD|CHEK)$/
&& ( ( $self->get('payinfo') ne $old->get('payinfo')
- && $self->get('payinfo') !~ /^99\d{14}$/
+ && !$self->tokenized
)
|| grep { $self->get($_) ne $old->get($_) } qw(paydate payname)
)
@@ -357,7 +357,7 @@ sub check {
or return gettext('invalid_card'); # . ": ". $self->payinfo;
my $cardtype = cardtype($payinfo);
- $cardtype = 'Tokenized' if $self->payinfo =~ /^99\d{14}$/; #token
+ $cardtype = 'Tokenized' if $self->tokenized; #token
return gettext('unknown_card_type') if $cardtype eq "Unknown";
@@ -546,7 +546,7 @@ sub check_payinfo_cardtype {
my $payinfo = $self->payinfo;
$payinfo =~ s/\D//g;
- if ( $payinfo =~ /^99\d{14}$/ ) {
+ if ( $self->tokenized($payinfo) ) {
$self->set('paycardtype', 'Tokenized');
return '';
}
diff --git a/FS/FS/geocode_Mixin.pm b/FS/FS/geocode_Mixin.pm
index a372faaa8..09b113112 100644
--- a/FS/FS/geocode_Mixin.pm
+++ b/FS/FS/geocode_Mixin.pm
@@ -273,7 +273,7 @@ sub process_district_update {
my $error = $self->replace;
die $error if $error;
- my %hash = map { $_ => $tax_info->{$_} }
+ my %hash = map { $_ => uc( $tax_info->{$_} ) }
qw( district city county state country );
$hash{'source'} = $method; # apply the update only to taxes we maintain
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 008ba8a86..35f178e25 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -1917,13 +1917,27 @@ sub calc_remain { 0; }
=item calc_units CUST_PKG
This returns the number of provisioned svc_phone records, or, of the package
-count_available_phones option is set, the number available to be provisoined
+count_available_phones option is set, the number available to be provisioned
in the package.
=cut
-#fallback that returns 0 for old legacy packages with no plan
-sub calc_units { 0; }
+sub calc_units {
+ my($self, $cust_pkg ) = @_;
+ my $count = 0;
+ if ( $self->option('count_available_phones', 1)) {
+ foreach my $pkg_svc ($cust_pkg->part_pkg->pkg_svc) {
+ if ($pkg_svc->part_svc->svcdb eq 'svc_phone') { # svc_pbx?
+ $count += $pkg_svc->quantity || 0;
+ }
+ }
+ $count *= $cust_pkg->quantity;
+ } else {
+ $count =
+ scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc);
+ }
+ $count;
+}
#fallback for everything not based on flat.pm
sub recur_temporality { 'upcoming'; }
diff --git a/FS/FS/part_pkg/global_Mixin.pm b/FS/FS/part_pkg/global_Mixin.pm
index e82602e1a..59eaaaaec 100644
--- a/FS/FS/part_pkg/global_Mixin.pm
+++ b/FS/FS/part_pkg/global_Mixin.pm
@@ -30,6 +30,10 @@ sub validate_moneyn {
return '';
}
+tie my %count_available_phones, 'Tie::IxHash', (
+ 0 => 'Provisioned phone services',
+ 1 => 'All available phone services',
+);
%info = (
'disabled' => 1,
@@ -63,6 +67,11 @@ sub validate_moneyn {
'name' => 'Automatic suspension period before cancelling (configuration setting part_pkg-delay_cancel-days)',
'type' => 'checkbox',
},
+ 'count_available_phones' => { 'name' => 'Count taxable phone lines',
+ 'type' => 'radio',
+ 'options' => \%count_available_phones,
+ 'default' => 0,
+ },
# miscellany--maybe put this in a separate module?
@@ -134,6 +143,8 @@ sub validate_moneyn {
unused_credit_change
delay_cancel
+ count_available_phones
+
a2billing_tariff
a2billing_type
a2billing_simultaccess
diff --git a/FS/FS/part_pkg/voip_cdr.pm b/FS/FS/part_pkg/voip_cdr.pm
index 420026dcb..9ecdba685 100644
--- a/FS/FS/part_pkg/voip_cdr.pm
+++ b/FS/FS/part_pkg/voip_cdr.pm
@@ -289,10 +289,6 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash',
'type' => 'checkbox',
},
- 'count_available_phones' => { 'name' => 'Consider for tax purposes the number of lines to be svc_phones that may be provisioned rather than those that actually are.',
- 'type' => 'checkbox',
- },
-
#XXX also have option for an external db? these days we suck them into ours
# 'cdr_location' => { 'name' => 'CDR database location'
# 'type' => 'select',
@@ -353,7 +349,7 @@ tie my %accountcode_tollfree_field, 'Tie::IxHash',
usage_mandate usage_section summarize_usage
usage_showzero bill_every_call bill_inactive_svcs
bill_only_pkg_dates
- count_available_phones suspend_bill
+ suspend_bill
)
],
'weight' => 41,
@@ -656,25 +652,6 @@ sub is_free {
0;
}
-# This equates svc_phone records; perhaps svc_phone should have a field
-# to indicate it represents a line
-sub calc_units {
- my($self, $cust_pkg ) = @_;
- my $count = 0;
- if ( $self->option('count_available_phones', 1)) {
- foreach my $pkg_svc ($cust_pkg->part_pkg->pkg_svc) {
- if ($pkg_svc->part_svc->svcdb eq 'svc_phone') { # svc_pbx?
- $count += $pkg_svc->quantity || 0;
- }
- }
- $count *= $cust_pkg->quantity;
- } else {
- $count =
- scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc);
- }
- $count;
-}
-
sub reset_usage {
my ($self, $cust_pkg, %opt) = @_;
my @part_pkg_usage = $self->part_pkg_usage or return '';
diff --git a/FS/FS/part_pkg/voip_inbound.pm b/FS/FS/part_pkg/voip_inbound.pm
index e911439c8..15af706c1 100644
--- a/FS/FS/part_pkg/voip_inbound.pm
+++ b/FS/FS/part_pkg/voip_inbound.pm
@@ -399,15 +399,5 @@ sub is_free {
0;
}
-# This equates svc_phone records; perhaps svc_phone should have a field
-# to indicate it represents a line
-# #XXX no count_available_phones?
-sub calc_units {
- my($self, $cust_pkg ) = @_;
- my $count =
- scalar(grep { $_->part_svc->svcdb eq 'svc_phone' } $cust_pkg->cust_svc);
- $count;
-}
-
1;
diff --git a/FS/FS/payinfo_Mixin.pm b/FS/FS/payinfo_Mixin.pm
index 3a32ad5b2..a0a2cbcc9 100644
--- a/FS/FS/payinfo_Mixin.pm
+++ b/FS/FS/payinfo_Mixin.pm
@@ -67,9 +67,9 @@ sub payinfo {
my($self,$payinfo) = @_;
if ( defined($payinfo) ) {
- $self->paymask($self->mask_payinfo) unless $self->payinfo =~ /^99\d{14}$/; #make sure old mask is set
+ $self->paymask($self->mask_payinfo) unless $self->tokenized; #make sure old mask is set
$self->setfield('payinfo', $payinfo);
- $self->paymask($self->mask_payinfo) unless $payinfo =~ /^99\d{14}$/; #remask unless tokenizing
+ $self->paymask($self->mask_payinfo) unless $self->tokenized($payinfo); #remask unless tokenizing
} else {
$self->getfield('payinfo');
}
@@ -130,7 +130,7 @@ sub mask_payinfo {
# Check to see if it's encrypted...
if ( ref($self) && $self->is_encrypted($payinfo) ) {
return 'N/A';
- } elsif ( $payinfo =~ /^99\d{14}$/ || $payinfo eq 'N/A' ) { #token
+ } elsif ( $self->tokenized($payinfo) || $payinfo eq 'N/A' ) { #token
return 'N/A (tokenized)'; #?
} else { # if not, mask it...
@@ -198,7 +198,7 @@ sub payinfo_check {
my $payinfo = $self->payinfo;
my $cardtype = cardtype($payinfo);
- $cardtype = 'Tokenized' if $payinfo =~ /^99\d{14}$/;
+ $cardtype = 'Tokenized' if $self->tokenized;
$self->set('paycardtype', $cardtype);
if ( $ignore_masked_payinfo and $self->mask_payinfo eq $self->payinfo ) {
@@ -233,6 +233,7 @@ sub payinfo_check {
}
}
+ return '';
}
=item payby_payinfo_pretty [ LOCALE ]
@@ -453,6 +454,15 @@ sub process_set_cardtype {
}
}
+sub tokenized {
+ my $self = shift;
+ my $payinfo = scalar(@_) ? shift : $self->payinfo;
+ ## or just $self->cust_main->tokenized($payinfo) ??
+ ## everything that currently uses this mixin is linked to cust_main,
+ ## but just in case, false laziness w/ FS::cust_main::Billing_Realtime
+ $payinfo =~ /^99\d{14}$/;
+}
+
=back
=head1 BUGS