use IO::Scalar;
use Data::Dumper;
use Digest::MD5 qw(md5_hex);
+use Digest::SHA qw(sha512_hex);
use Date::Format;
use Time::Duration;
use Time::Local qw(timelocal_nocheck);
#use FS::UID qw(dbh);
use FS::Record qw(qsearch qsearchs dbh);
use FS::Msgcat qw(gettext);
-use FS::Misc qw(card_types);
+use FS::Misc qw(card_types money_pretty);
use FS::Misc::DateTime qw(parse_datetime);
use FS::TicketSystem;
use FS::ClientAPI_SessionCache;
use FS::payby;
use FS::acct_rt_transaction;
use FS::msg_template;
+use FS::contact;
+use FS::cust_contact;
+use FS::cust_location;
+use FS::cust_payby;
-$DEBUG = 1;
+# for code organization
+use FS::ClientAPI::MyAccount::contact;
+use FS::ClientAPI::MyAccount::quotation;
+
+$DEBUG = 0;
$me = '[FS::ClientAPI::MyAccount]';
use vars qw( @cust_main_editable_fields @location_editable_fields );
#return { 'error' => $session } if $context eq 'error';
my $agentnum = '';
- if ( $context eq 'customer' ) {
+ if ( $context eq 'customer' && $custnum ) {
my $sth = dbh->prepare('SELECT agentnum FROM cust_main WHERE custnum = ?')
or die dbh->errstr;
} elsif ( defined($p->{'agentnum'}) and $p->{'agentnum'} =~ /^(\d+)$/ ) {
$agentnum = $1;
}
+ $p->{'agentnum'} = $agentnum;
my $conf = new FS::Conf;
),
'menu_disable' => [ $conf->config('selfservice-menu_disable',$agentnum) ],
( map { $_ => $conf->exists("selfservice-$_", $agentnum ) }
- qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo )
+ qw( menu_skipblanks menu_skipheadings menu_nounderline no_logo enable_payment_without_balance )
),
( map { $_ => scalar($conf->config_binary("selfservice-$_", $agentnum)) }
qw( title_left_image title_right_image
'logo' => scalar($conf->config_binary('logo.png', $agentnum )),
( map { $_ => join("\n", $conf->config("selfservice-$_", $agentnum ) ) }
qw( head body_header body_footer company_address ) ),
+ 'money_char' => $conf->config("money_char") || '$',
+ 'menu' => join("\n", $conf->config("ng_selfservice-menu", $agentnum ) ) ||
+ 'main.php Home
+
+ services.php Services
+ services.php My Services
+ services_new.php Order a new service
+
+ personal.php Profile
+ personal.php Personal Information
+ password.php Change Password
+
+ payment.php Payments
+ payment_cc.php Credit Card Payment
+ payment_ach.php Electronic Check Payment
+ payment_paypal.php PayPal Payment
+ payment_webpay.php Webpay Payments
+
+ usage.php Usage
+ usage_data.php Data usage
+ usage_cdr.php Call usage
+
+ tickets.php Help Desk
+ tickets.php Open Tickets
+ tickets_resolved.php Resolved Tickets
+ ticket_create.php Create a new ticket
+
+ docs.php FAQs
+
+ logout.php Logout
+ ',
};
_cache->set("skin_info_cache_agent$agentnum", $skin_info_cache_agent);
my $conf = new FS::Conf;
my $svc_x = '';
+ my $session = {};
if ( $p->{'domain'} eq 'svc_phone'
&& $conf->exists('selfservice_server-phone_login') ) {
$svc_x = $svc_phone;
+ } elsif ( $p->{email}
+ && (my $contact = FS::contact->by_selfservice_email($p->{email}))
+ )
+ {
+ return { error => 'Incorrect contact password.' }
+ unless $contact->authenticate_password($p->{'password'});
+
+ $session->{'contactnum'} = $contact->contactnum;
+
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ if ( scalar(@cust_contact) == 1 ) {
+ $session->{'custnum'} = $cust_contact[0]->custnum;
+ } elsif ( scalar(@cust_contact) ) {
+ $session->{'customers'} = { map { $_->custnum => $_->cust_main->name }
+ @cust_contact
+ };
+ } else {
+ return { error => 'No customer self-service access for contact' }; #??
+ }
+
} else {
+ ( $p->{username}, $p->{domain} ) = split('@', $p->{email}) if $p->{email};
+
my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
or return { error => 'Domain '. $p->{'domain'}. ' not found' };
- my $svc_acct = qsearchs( 'svc_acct', { 'username' => $p->{'username'},
- 'domsvc' => $svc_domain->svcnum, }
- );
- return { error => 'User not found.' } unless $svc_acct;
+ my @svc_acct = qsearch( 'svc_acct', { 'username' => $p->{'username'},
+ 'domsvc' => $svc_domain->svcnum, }
+ );
+
+ if ( $conf->exists('selfservice_server-login_svcpart') ) {
+ my @svcpart = $conf->config('selfservice_server-login_svcpart');
+ @svc_acct = grep { my $svcpart = $_->cust_svc->svcpart;
+ scalar( grep( $_ eq $svcpart, @svcpart ) );
+ }
+ @svc_acct;
+ }
+
+ if ( $conf->exists('selfservice_server-primary_only') ) {
+ @svc_acct =
+ grep {
+ my $cust_svc = $_->cust_svc;
+ $cust_svc->cust_pkg->part_pkg->svcpart([qw( svc_acct svc_phone )])
+ == $cust_svc->svcpart
+ }
+ @svc_acct;
+ }
+
+ return { error => 'User not found.' } unless @svc_acct;
+
+ return { error => 'Multiple users.' } if scalar(@svc_acct) > 1;
+
+ my $svc_acct = $svc_acct[0];
- if($conf->exists('selfservice_server-login_svcpart')) {
- my @svcpart = $conf->config('selfservice_server-login_svcpart');
- my $svcpart = $svc_acct->cust_svc->svcpart;
- return { error => 'Invalid user.' }
- unless grep($_ eq $svcpart, @svcpart);
+ if ( $conf->exists('selfservice_server-login_svcpart') ) {
+ my @svcpart = $conf->config('selfservice_server-login_svcpart');
+ my $svcpart = $svc_acct->cust_svc->svcpart;
+ return { error => 'Invalid user.' }
+ unless grep($_ eq $svcpart, @svcpart);
}
return { error => 'Incorrect password.' }
}
- my $session = {
- 'svcnum' => $svc_x->svcnum,
- };
+ if ( $svc_x ) {
- my $cust_svc = $svc_x->cust_svc;
- my $cust_pkg = $cust_svc->cust_pkg;
- if ( $cust_pkg ) {
- my $cust_main = $cust_pkg->cust_main;
- $session->{'custnum'} = $cust_main->custnum;
- if ( $conf->exists('pkg-balances') ) {
- my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
- $cust_main->ncancelled_pkgs;
- $session->{'pkgnum'} = $cust_pkg->pkgnum
- if scalar(@cust_pkg) > 1;
+ $session->{'svcnum'} = $svc_x->svcnum;
+
+ my $cust_svc = $svc_x->cust_svc;
+ my $cust_pkg = $cust_svc->cust_pkg;
+ if ( $cust_pkg ) {
+ my $cust_main = $cust_pkg->cust_main;
+ $session->{'custnum'} = $cust_main->custnum;
+ if ( $conf->exists('pkg-balances') ) {
+ my @cust_pkg = grep { $_->part_pkg->freq !~ /^(0|$)/ }
+ $cust_main->ncancelled_pkgs;
+ $session->{'pkgnum'} = $cust_pkg->pkgnum
+ if scalar(@cust_pkg) > 1;
+ }
}
- }
- #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
- #return { error => 'Only primary user may log in.' }
- # if $conf->exists('selfservice_server-primary_only')
- # && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
- my $part_pkg = $cust_pkg->part_pkg;
- return { error => 'Only primary user may log in.' }
- if $conf->exists('selfservice_server-primary_only')
- && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
+ #my $pkg_svc = $svc_acct->cust_svc->pkg_svc;
+ #return { error => 'Only primary user may log in.' }
+ # if $conf->exists('selfservice_server-primary_only')
+ # && ( ! $pkg_svc || $pkg_svc->primary_svc ne 'Y' );
+ my $part_pkg = $cust_pkg->part_pkg;
+ return { error => 'Only primary user may log in.' }
+ if $conf->exists('selfservice_server-primary_only')
+ && $cust_svc->svcpart != $part_pkg->svcpart([qw( svc_acct svc_phone )]);
+
+ }
my $session_id;
do {
- $session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
+ $session_id = sha512_hex(time(). {}. rand(). $$)
} until ( ! defined _cache->get($session_id) ); #just in case
my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
return { 'error' => '',
'session_id' => $session_id,
+ %$session,
};
}
sub logout {
my $p = shift;
+ my $skin_info = skin_info($p);
if ( $p->{'session_id'} ) {
_cache->remove($p->{'session_id'});
- return { %{ skin_info($p) }, 'error' => '' };
+ return { %$skin_info, 'error' => '' };
} else {
- return { %{ skin_info($p) }, 'error' => "Can't resume session" }; #better error message
+ return { %$skin_info, 'error' => "Can't resume session" }; #better error message
}
}
}
+sub switch_cust {
+ my $p = shift;
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ $session->{'custnum'} = $p->{'custnum'}
+ if exists $session->{'customers'}{ $p->{'custnum'} };
+
+ my $conf = new FS::Conf;
+ my $timeout = $conf->config('selfservice-session_timeout') || '1 hour';
+ _cache->set( $p->{'session_id'}, $session, $timeout );
+
+ return { 'error' => '',
+ %{ customer_info( { session_id=>$p->{'session_id'} } ) },
+ };
+}
+
sub payment_gateway {
# internal use only
# takes a cust_main and a cust_payby entry, returns the payment_gateway
my($context, $session, $custnum) = _custoragent_session_custnum($p);
return { 'error' => $session } if $context eq 'error';
- my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
- or return { 'error' => "unknown custnum $custnum" };
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } );
$info->{'hide_payment_fields'} = [
map {
- my $pg = payment_gateway($cust_main, $_);
+ my $pg = $cust_main && payment_gateway($cust_main, $_);
$pg && $pg->gateway_namespace eq 'Business::OnlineThirdPartyPayment';
} @{ $info->{cust_paybys} }
];
$info->{'self_suspend_reason'} =
- $conf->config('selfservice-self_suspend_reason', $cust_main->agentnum);
+ $conf->config('selfservice-self_suspend_reason',
+ $cust_main ? $cust_main->agentnum : ''
+ );
$info->{'edit_ticket_subject'} =
$conf->exists('ticket_system-selfservice_edit_subject') &&
- $cust_main->edit_subject;
+ $cust_main && $cust_main->edit_subject;
+
+ $info->{'timeout'} = $conf->config('selfservice-timeout') || 3600;
+
+ $info->{'hide_usage'} = $conf->exists('selfservice_hide-usage');
return { %$info,
'custnum' => $custnum,
my %return;
my $conf = new FS::Conf;
- if ($conf->exists('cust_main-require_address2')) {
- $return{'require_address2'} = '1';
- }else{
- $return{'require_address2'} = '';
- }
+ $return{'require_address2'} = $conf->exists('cust_main-require_address2');
- if ( $FS::TicketSystem::system ) {
- warn "$me customer_info: initializing ticket system\n" if $DEBUG;
- FS::TicketSystem->init();
- }
+# if ( $FS::TicketSystem::system ) {
+# warn "$me customer_info: initializing ticket system\n" if $DEBUG;
+# FS::TicketSystem->init();
+# }
if ( $custnum ) { #customer record
+ %return = ( %return, %{ customer_info_short($p) } );
+
+ #redundant with customer_info_short, but we need it for several things below
my $search = { 'custnum' => $custnum };
$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
my $cust_main = qsearchs('cust_main', $search )
- or return { 'error' => "unknown custnum $custnum" };
+ or return { 'error' => "customer_info: unknown custnum $custnum" };
- $return{display_custnum} = $cust_main->display_custnum;
+ my $list_tickets = list_tickets($p);
+ $return{'tickets'} = $list_tickets->{'tickets'};
- if ( $session->{'pkgnum'} ) {
- $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
- #next_bill_date from cust_pkg?
+ if ( $session->{'pkgnum'} ) {
+ #XXX open invoices in the pkg-balances case
} else {
- $return{balance} = $cust_main->balance;
- $return{next_bill_date} = $cust_main->next_bill_date;
- $return{next_bill_date_pretty} =
- $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
- : '(none)';
- }
-
- my @tickets = $cust_main->tickets;
- # unavoidable false laziness w/ httemplate/view/cust_main/tickets.html
- if ( $FS::TicketSystem::system && FS::TicketSystem->selfservice_priority ) {
- my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1;
- $return{tickets} = [
- sort {
- (
- ($a->{'_selfservice_priority'} eq '') <=>
- ($b->{'_selfservice_priority'} eq '')
- ) ||
- ( $dir *
- ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'})
- )
- } @tickets
- ];
- }
- else {
- $return{tickets} = \@tickets;
- }
-
- unless ( $session->{'pkgnum'} ) {
+ $return{'money_char'} = $conf->config("money_char") || '$';
my @open = map {
{
- invnum => $_->invnum,
- date => time2str("%b %o, %Y", $_->_date),
- owed => $_->owed,
+ invnum => $_->invnum,
+ date => time2str("%b %o, %Y", $_->_date),
+ owed => $_->owed,
+ charged => $_->charged,
};
} $cust_main->open_cust_bill;
$return{open_invoices} = \@open;
time2str('%m/%d/%Y', $return{'last_invoice_date'} );
}
- $return{countrydefault} = scalar($conf->config('countrydefault'));
-
+ #customer_info_short always has nobalance on..
$return{small_custview} =
small_custview( $cust_main,
$return{countrydefault},
( $session->{'pkgnum'} ? 1 : 0 ), #nobalance
);
- $return{name} = $cust_main->first. ' '. $cust_main->get('last');
-
$return{has_ship_address} = $cust_main->has_ship_address;
- $return{status} = $cust_main->status;
+ $return{status} = $cust_main->status_label; #$cust_main->status; #better to break anyone obscurely testing for strings in self-service than to have to upgrade every front-end to get the new status to display
$return{statuscolor} = $cust_main->statuscolor;
+ $return{status_label} = $cust_main->status_label;
- for (@cust_main_editable_fields) {
- $return{$_} = $cust_main->get($_);
- }
-
- for (@location_editable_fields) {
- $return{$_} = $cust_main->bill_location->get($_);
- $return{'ship_'.$_} = $cust_main->ship_location->get($_);
- }
- $return{has_ship_address} = $cust_main->has_ship_address;
# compatibility: some places in selfservice use this to determine
# if there's a ship address
if ( $return{has_ship_address} ) {
$return{ship_first} = $cust_main->first;
}
- if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
- $return{payinfo} = $cust_main->paymask;
- @return{'month', 'year'} = $cust_main->paydate_monthyear;
- }
-
- $return{'invoicing_list'} =
- join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
- $return{'postal_invoicing'} =
- 0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
-
if (scalar($conf->config('support_packages'))) {
my @support_services = ();
foreach ($cust_main->support_services) {
$return{discount_terms_hash} = { $cust_main->discount_terms_hash };
}
- if ( $session->{'svcnum'} ) {
- my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} });
- $return{'svc_label'} = ($cust_svc->label)[1] if $cust_svc;
- $return{'svcnum'} = $session->{'svcnum'};
- }
-
} elsif ( $session->{'svcnum'} ) { #no customer record
my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $session->{'svcnum'} } )
my $search = { 'custnum' => $custnum };
$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
my $cust_main = qsearchs('cust_main', $search )
- or return { 'error' => "unknown custnum $custnum" };
+ or return { 'error' => "customer_info_short: unknown custnum $custnum" };
$return{display_custnum} = $cust_main->display_custnum;
+ if ( $session->{'pkgnum'} ) {
+ $return{balance} = $cust_main->balance_pkgnum( $session->{'pkgnum'} );
+ #next_bill_date from cust_pkg?
+ } else {
+ $return{balance} = $cust_main->balance;
+ $return{next_bill_date} = $cust_main->next_bill_date;
+ $return{next_bill_date_pretty} =
+ $return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
+ : '(none)';
+ }
+ $return{balance_pretty} = money_pretty($return{balance});
+
$return{countrydefault} = scalar($conf->config('countrydefault'));
$return{small_custview} =
1, ##nobalance
);
- $return{name} = $cust_main->first. ' '. $cust_main->get('last');
+ $return{first} = $cust_main->first;
+ $return{'last'} = $cust_main->get('last');
+ $return{name} = $cust_main->first. ' '. $cust_main->get('last');
$return{payby} = $cust_main->payby;
}
#maybe a little more expensive, but it should be cached by now
for (@location_editable_fields) {
- $return{$_} = $cust_main->bill_location->get($_);
- $return{'ship_'.$_} = $cust_main->ship_location->get($_);
+ $return{$_} = $cust_main->bill_location->get($_)
+ if $cust_main->bill_locationnum;
+ $return{'ship_'.$_} = $cust_main->ship_location->get($_)
+ if $cust_main->ship_locationnum;
}
if ( $cust_main->payby =~ /^(CARD|DCRD)$/ ) {
@return{'month', 'year'} = $cust_main->paydate_monthyear;
}
+ my @invoicing_list = $cust_main->invoicing_list;
$return{'invoicing_list'} =
- join(', ', grep { $_ !~ /^(POST|FAX)$/ } $cust_main->invoicing_list );
- #$return{'postal_invoicing'} =
- # 0 < ( grep { $_ eq 'POST' } $cust_main->invoicing_list );
+ join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
+ $return{'postal_invoicing'} =
+ 0 < ( grep { $_ eq 'POST' } @invoicing_list );
if ( $session->{'svcnum'} ) {
my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $session->{'svcnum'} });
}
+ # this is here because this routine is called by both fs_ and ng_ main pages, where it appears
+ # it is not customer-specific, though it is only shown to authenticated customers
+ # it is not currently agent-specific, though at some point it might be
+ $return{'announcement'} = join(' ',$conf->config('selfservice-announcement')) || '';
+
return { 'error' => '',
'custnum' => $custnum,
%return,
}
$return{balance} = $cust_main->balance;
+ $return{balance_pretty} = money_pretty($return{balance});
$return{next_bill_date} = $cust_main->next_bill_date;
$return{next_bill_date_pretty} =
$return{next_bill_date} ? time2str('%m/%d/%Y', $return{next_bill_date} )
: '(none)';
- my @history = ();
-
my $conf = new FS::Conf;
- if ( $conf->exists('selfservice-billing_history-line_items') ) {
-
- foreach my $cust_bill ( $cust_main->cust_bill ) {
-
- push @history, {
- 'type' => 'Line item',
- 'description' => $_->desc( $cust_main->locale ).
- ( $_->sdate && $_->edate
- ? ' '. time2str('%d-%b-%Y', $_->sdate).
- ' To '. time2str('%d-%b-%Y', $_->edate)
- : ''
- ),
- 'amount' => sprintf('%.2f', $_->setup + $_->recur ),
- 'date' => $cust_bill->_date,
- 'date_pretty' => time2str('%m/%d/%Y', $cust_bill->_date ),
- }
- foreach $cust_bill->cust_bill_pkg;
-
- }
-
- } else {
+ $return{'history'} = [
+ $cust_main->payment_history(
+ 'line_items' => $conf->exists('selfservice-billing_history-line_items'),
+ 'reverse_sort' => 1,
+ )
+ ];
- push @history, {
- 'type' => 'Invoice',
- 'description' => 'Invoice #'. $_->display_invnum,
- 'amount' => sprintf('%.2f', $_->charged ),
- 'date' => $_->_date,
- 'date_pretty' => time2str('%m/%d/%Y', $_->_date ),
- }
- foreach $cust_main->cust_bill;
-
- }
-
- push @history, {
- 'type' => 'Payment',
- 'description' => 'Payment', #XXX type
- 'amount' => sprintf('%.2f', 0 - $_->paid ),
- 'date' => $_->_date,
- 'date_pretty' => time2str('%m/%d/%Y', $_->_date ),
- }
- foreach $cust_main->cust_pay;
-
- push @history, {
- 'type' => 'Credit',
- 'description' => 'Credit', #more info?
- 'amount' => sprintf('%.2f', 0 -$_->amount ),
- 'date' => $_->_date,
- 'date_pretty' => time2str('%m/%d/%Y', $_->_date ),
- }
- foreach $cust_main->cust_credit;
-
- push @history, {
- 'type' => 'Refund',
- 'description' => 'Refund', #more info? type, like payment?
- 'amount' => $_->refund,
- 'date' => $_->_date,
- 'date_pretty' => time2str('%m/%d/%Y', $_->_date ),
- }
- foreach $cust_main->cust_refund;
-
- @history = sort { $b->{'date'} <=> $a->{'date'} } @history;
-
- $return{'history'} = \@history;
+ $return{'money_char'} = $conf->config("money_char") || '$',
return \%return;
$payby = $1;
}
+ my $conf = new FS::Conf;
+
if ( $payby =~ /^(CARD|DCRD)$/ ) {
$new->paydate($p->{'year'}. '-'. $p->{'month'}. '-01');
if ( $new->payinfo eq $cust_main->paymask ) {
$new->payinfo($cust_main->payinfo);
+ $new->paycvv( $p->{'paycvv'} || $cust_main->paycvv );
} else {
$new->payinfo($p->{'payinfo'});
+ return { 'error' => 'CVV2 is required' }
+ if ! $p->{'paycvv'} && $conf->exists('selfservice-onfile_require_cvv');
+ $new->paycvv( $p->{'paycvv'} )
}
$new->set( 'payby' => $p->{'auto'} ? 'CARD' : 'DCRD' );
'card_types' => card_types(),
- 'paytypes' => [ @FS::cust_main::paytypes ],
+ 'withcvv' => $conf->exists('selfservice-require_cvv'), #or enable optional cvv?
+ 'require_cvv' => $conf->exists('selfservice-require_cvv'),
+ 'onfile_require_cvv' => $conf->exists('selfservice-onfile_require_cvv'),
+
+ 'paytypes' => [ FS::cust_payby::paytypes ],
'paybys' => [ $conf->config('signup_server-payby') ],
'cust_paybys' => \@cust_paybys,
'save_unchecked' => $conf->exists('selfservice-save_unchecked'),
- 'credit_card_surcharge_percentage' => $conf->config('credit-card-surcharge-percentage'),
+ 'credit_card_surcharge_percentage' => scalar($conf->config('credit-card-surcharge-percentage')),
};
}
#doubleclick protection
my $_date = time;
- $return{paybatch} = "webui-MyAccount-$_date-$$-". rand() * 2**32;
+ $return{payunique} = "webui-MyAccount-$_date-$$-". rand() * 2**32; #new
+ $return{paybatch} = $return{payunique}; #back compat
return { 'error' => '',
%return,
or return { 'error' => gettext('illegal_name'). " payname: ". $p->{'payname'} };
my $payname = $1;
+ $p->{'payunique'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
+ or return { 'error' => gettext('illegal_text'). " payunique: ". $p->{'payunique'} };
+ my $payunique = $1;
+
$p->{'paybatch'} =~ /^([\w \!\@\#\$\%\&\(\)\-\+\;\:\'\"\,\.\?\/\=]*)$/
or return { 'error' => gettext('illegal_text'). " paybatch: ". $p->{'paybatch'} };
my $paybatch = $1;
+ $payunique = $paybatch if ! length($payunique) && length($paybatch);
+
$p->{'payby'} ||= 'CARD';
$p->{'payby'} =~ /^([A-Z]{4})$/
or return { 'error' => "illegal_payby " . $p->{'payby'} };
$payinfo = $p->{'payinfo'};
+ my $onfile = 0;
+
#more intelligent matching will be needed here if you change
#card_masking_method and don't remove existing paymasks
- $payinfo = $cust_main->payinfo
- if $cust_main->paymask eq $payinfo;
+ if ( $cust_main->paymask eq $payinfo ) {
+ $payinfo = $cust_main->payinfo;
+ $onfile = 1;
+ }
$payinfo =~ s/\D//g;
$payinfo =~ /^(\d{13,16}|\d{8,9})$/
or return { 'error' => "CVV2 (CVC2/CID) is three digits." };
$paycvv = $1;
}
+ } elsif ( $conf->exists('selfservice-onfile_require_cvv') ) {
+ return { 'error' => 'CVV2 is required' };
+ } elsif ( !$onfile && $conf->exists('selfservice-require_cvv') ) {
+ return { 'error' => 'CVV2 is required' };
}
} else {
{
'cust_main' => $cust_main, #XXX or just custnum??
- 'amount' => $amount,
+ 'amount' => sprintf('%.2f', $amount),
'payby' => $payby,
'payinfo' => $payinfo,
'paymask' => $cust_main->mask_payinfo( $payby, $payinfo ),
'month' => $p->{'month'},
'year' => $p->{'year'},
'payname' => $payname,
- 'paybatch' => $paybatch, #this doesn't actually do anything
+ 'payunique' => $payunique,
+ 'paybatch' => $paybatch,
'paycvv' => $paycvv,
'payname' => $payname,
'discount_term' => $discount_term,
my $payby = delete $validate->{'payby'};
- my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
- 'quiet' => 1,
- 'selfservice' => 1,
- 'paynum_ref' => \$paynum,
- %$validate,
- );
- return { 'error' => $error } if $error;
-
- #no error, so order the fee package if applicable...
- my $conf = new FS::Conf;
- my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
- my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
-
- if ( $fee_pkgpart and ! $fee_skip_first || scalar($cust_main->cust_pay) ) {
-
- my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart };
-
- $error = $cust_main->order_pkg( 'cust_pkg' => $cust_pkg );
- return { 'error' => "payment processed successfully, but error ordering fee: $error" }
- if $error;
-
- #and generate an invoice for it now too
- $error = $cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
- return { 'error' => "payment processed and fee ordered sucessfully, but error billing fee: $error" }
- if $error;
-
- }
-
- $cust_main->apply_payments;
-
if ( $validate->{'save'} ) {
my $new = new FS::cust_main { $cust_main->hash };
if ($payby eq 'CARD' || $payby eq 'DCRD') {
stateid stateid_state );
$new->set( 'payby' => $validate->{'auto'} ? 'CHEK' : 'DCHK' );
}
- $new->set( 'payinfo' => $cust_main->card_token || $validate->{'payinfo'} );
+ $new->payinfo( $validate->{'payinfo'} ); #to properly set paymask
$new->set( 'paydate' => $validate->{'paydate'} );
my $error = $new->replace($cust_main);
if ( $error ) {
#return { 'error' => $error };
#XXX just warn verosely for now so i can figure out how these happen in
# the first place, eventually should redirect them to the "change
- #address" page but indicate the payment did process??
+ #address" page but indicate if the payment processed?
delete($validate->{'payinfo'}); #don't want to log this!
warn "WARNING: error changing customer info when processing payment (not returning to customer as a processing error): $error\n".
"NEW: ". Dumper($new)."\n".
"OLD: ". Dumper($cust_main)."\n".
"PACKET: ". Dumper($validate)."\n";
- #} else {
- #not needed...
- #$cust_main = $new;
+ } else {
+ $cust_main = $new;
}
}
+ my $error = $cust_main->realtime_bop( $FS::payby::payby2bop{$payby}, $amount,
+ 'quiet' => 1,
+ 'manual' => 1,
+ 'selfservice' => 1,
+ 'paynum_ref' => \$paynum,
+ %$validate,
+ );
+ return { 'error' => $error } if $error;
+
+ #no error, so order the fee package if applicable...
+ my $conf = new FS::Conf;
+ my $fee_pkgpart = $conf->config('selfservice_process-pkgpart', $cust_main->agentnum);
+ my $fee_skip_first = $conf->exists('selfservice_process-skip_first');
+
+ if ( $fee_pkgpart and ! $fee_skip_first || scalar($cust_main->cust_pay) ) {
+
+ my $cust_pkg = new FS::cust_pkg { 'pkgpart' => $fee_pkgpart };
+
+ $error = $cust_main->order_pkg( 'cust_pkg' => $cust_pkg );
+ return { 'error' => "payment processed successfully, but error ordering fee: $error" }
+ if $error;
+
+ #and generate an invoice for it now too
+ $error = $cust_main->bill( 'pkg_list' => [ $cust_pkg ] );
+ return { 'error' => "payment processed and fee ordered sucessfully, but error billing fee: $error" }
+ if $error;
+
+ }
+
+ $cust_main->apply_payments;
+
my $cust_pay = '';
my $receipt_html = '';
if ($paynum) {
if ( $cust_pay ) {
- my($gw, $auth, $order) = split(':', $cust_pay->paybatch);
-
return {
'error' => '',
'amount' => sprintf('%.2f', $cust_pay->paid),
'date' => $cust_pay->_date,
'date_pretty' => time2str('%Y-%m-%d', $cust_pay->_date),
'time_pretty' => time2str('%T', $cust_pay->_date),
- 'auth_num' => $auth,
- 'order_num' => $order,
+ 'auth_num' => $cust_pay->auth,
+ 'order_num' => $cust_pay->order_number,
'receipt_html' => $receipt_html,
};
return { 'error' => '', amount => $amount, %$error };
}
+sub start_thirdparty {
+ my $p = shift;
+ my $session = _cache->get($p->{'session_id'})
+ or return { 'error' => "Can't resume session" }; #better error message
+ my $custnum = $session->{'custnum'};
+ my $cust_main = FS::cust_main->by_key($custnum);
+
+ my $amount = $p->{'amount'}
+ or return { error => 'no amount' };
+
+ my $result = $cust_main->create_payment(
+ 'method' => $p->{'method'},
+ 'amount' => $p->{'amount'},
+ 'pkgnum' => $session->{'pkgnum'},
+ 'session_id' => $p->{'session_id'},
+ );
+
+ if ( ref($result) ) { # hashref or error
+ return $result;
+ } else {
+ return { error => $result };
+ }
+}
+
+sub finish_thirdparty {
+ my $p = shift;
+ my $session_id = delete $p->{'session_id'};
+ my $session = _cache->get($session_id)
+ or return { 'error' => "Can't resume session" };
+ my $custnum = $session->{'custnum'};
+ my $cust_main = FS::cust_main->by_key($custnum);
+
+ if ( $p->{_cancel} ) {
+ # customer backed out of making a payment
+ return $cust_main->cancel_payment( $session_id );
+ }
+ my $result = $cust_main->execute_payment( $session_id, %$p );
+ if ( ref($result) ) {
+ return $result;
+ } else {
+ return { error => $result };
+ }
+}
+
sub process_payment_order_pkg {
my $p = shift;
my @cust_bill = grep ! $_->hide, $cust_main->cust_bill;
my $balance = 0;
+ my $invoices = [
+ map {
+ #not super efficient, we also run cust_bill_pay/cust_credited inside owed
+ my @payments_and_credits = sort {$b->_date <=> $a->_date} ($_->cust_bill_pay,$_->cust_credited);
+ my $owed = $_->owed;
+ $balance += $owed;
+ +{ 'invnum' => $_->invnum,
+ '_date' => $_->_date,
+ 'date' => time2str("%b %o, %Y", $_->_date),
+ 'date_short' => time2str("%m-%d-%Y", $_->_date),
+ 'previous' => sprintf('%.2f', ($_->previous)[0]),
+ 'charged' => sprintf('%.2f', $_->charged),
+ 'owed' => sprintf('%.2f', $owed),
+ 'balance' => sprintf('%.2f', $balance),
+ 'lastpay' => @payments_and_credits
+ ? time2str("%b %o, %Y", $payments_and_credits[0]->_date)
+ : '',
+ }
+ } @cust_bill
+ ];
return { 'error' => '',
'balance' => $cust_main->balance,
- 'invoices' => [
- map {
- my $owed = $_->owed;
- $balance += $owed;
- +{ 'invnum' => $_->invnum,
- '_date' => $_->_date,
- 'date' => time2str("%b %o, %Y", $_->_date),
- 'date_short' => time2str("%m-%d-%Y", $_->_date),
- 'previous' => sprintf('%.2f', ($_->previous)[0]),
- 'charged' => sprintf('%.2f', $_->charged),
- 'owed' => sprintf('%.2f', $owed),
- 'balance' => sprintf('%.2f', $balance),
- }
- }
- @cust_bill
- ],
+ 'money_char' => $conf->config("money_char") || '$',
+ 'invoices' => $invoices,
'legacy_invoices' => [
map {
+{ 'legacyinvnum' => $_->legacyinvnum,
or return { 'error' => "unknown custnum $custnum" };
my $conf = new FS::Conf;
+ my $immutable = $conf->exists('selfservice_immutable-package');
# the duplication below is necessary:
# 1. to maintain the current buggy behaviour wrt the cust_pkg and part_pkg
'custnum' => $custnum,
'cust_pkg' => [ map {
{ $_->hash,
+ immutable => $immutable,
part_pkg => [ map $_->hashref, $_->part_pkg ],
part_svc =>
[ map $_->hashref, $_->available_part_svc ],
my $primary_cust_svc = $_->primary_cust_svc;
+{ $_->hash,
$_->part_pkg->hash,
- pkg_label => $_->pkg_locale,
- status => $_->status,
+ immutable => $immutable,
+ pkg_label => $_->pkg_locale,
+ status => $_->status,
+ statuscolor => $_->statuscolor,
part_svc =>
[ map { $_->hashref }
grep { $_->selfservice_access ne 'hidden' }
],
'small_custview' =>
small_custview( $cust_main, $conf->config('countrydefault') ),
+ 'date_format' => $conf->config('date_format') || '%m/%d/%Y',
};
}
my($context, $session, $custnum) = _custoragent_session_custnum($p);
return { 'error' => $session } if $context eq 'error';
+ my $conf = new FS::Conf;
+
+ my $hide_usage = $conf->exists('selfservice_hide-usage') ? 1 : 0;
my $search = { 'custnum' => $custnum };
$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
my $cust_main = qsearchs('cust_main', $search )
my @cust_svc = ();
my @cust_pkg_usage = ();
- #foreach my $cust_pkg ( $cust_main->ncancelled_pkgs ) {
foreach my $cust_pkg ( $p->{'ncancelled'}
? $cust_main->ncancelled_pkgs
: $cust_main->unsuspended_pkgs ) {
@cust_svc = grep { $_->part_svc->selfservice_access ne 'hidden' } @cust_svc;
my %usage_pools;
- foreach (@cust_pkg_usage) {
- my $part = $_->part_pkg_usage;
- my $tag = $part->description . ($part->shared ? 1 : 0);
- my $row = $usage_pools{$tag}
- ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
- $row->[1] += $_->minutes; # minutes remaining
- $row->[2] += $part->minutes; # minutes total
- }
+ if (!$hide_usage) {
+ foreach (@cust_pkg_usage) {
+ my $part = $_->part_pkg_usage;
+ my $tag = $part->description . ($part->shared ? 1 : 0);
+ my $row = $usage_pools{$tag}
+ ||= [ $part->description, 0, 0, $part->shared ? 1 : 0 ];
+ $row->[1] += sprintf('%.1f', $_->minutes); # minutes remaining
+ $row->[2] += $part->minutes; # minutes total
+ }
+ } # otherwise just leave them empty
if ( $p->{'svcdb'} ) {
my $svcdb = ref($p->{'svcdb'}) eq 'HASH'
#@svc_x = sort { $a->domain cmp $b->domain || $a->username cmp $b->username }
# @svc_x;
- my $conf = new FS::Conf;
+ my @svcs; # stuff to return to the client
+ foreach my $cust_svc (@cust_svc) {
+ my $svc_x = $cust_svc->svc_x;
+ my($label, $value) = $cust_svc->label;
+ my $part_svc = $cust_svc->part_svc;
+ my $svcdb = $part_svc->svcdb;
+ my $cust_pkg = $cust_svc->cust_pkg;
+ my $part_pkg = $cust_pkg->part_pkg;
+
+ my %hash = (
+ 'svcnum' => $cust_svc->svcnum,
+ 'display_svcnum' => $cust_svc->display_svcnum,
+ 'svcdb' => $svcdb,
+ 'label' => $label,
+ 'value' => $value,
+ 'pkg_label' => $cust_pkg->pkg_locale,
+ 'pkg_status' => $cust_pkg->status,
+ 'readonly' => ($part_svc->selfservice_access eq 'readonly'),
+ );
- {
+ # would it make sense to put this in a svc_* method?
+
+ if ( $svcdb eq 'svc_acct' ) {
+ foreach (qw(username email finger seconds)) {
+ $hash{$_} = $svc_x->$_;
+ }
+
+ if (!$hide_usage) {
+ %hash = (
+ %hash,
+ 'upbytes' => display_bytecount($svc_x->upbytes),
+ 'downbytes' => display_bytecount($svc_x->downbytes),
+ 'totalbytes' => display_bytecount($svc_x->totalbytes),
+
+ 'recharge_amount' => $part_pkg->option('recharge_amount',1),
+ 'recharge_seconds' => $part_pkg->option('recharge_seconds',1),
+ 'recharge_upbytes' =>
+ display_bytecount($part_pkg->option('recharge_upbytes',1)),
+ 'recharge_downbytes' =>
+ display_bytecount($part_pkg->option('recharge_downbytes',1)),
+ 'recharge_totalbytes' =>
+ display_bytecount($part_pkg->option('recharge_totalbytes',1)),
+ # more...
+ );
+ }
+
+ } elsif ( $svcdb eq 'svc_dsl' ) {
+
+ $hash{'phonenum'} = $svc_x->phonenum;
+ if ( $svc_x->first || $svc_x->get('last') || $svc_x->company ) {
+ $hash{'name'} = $svc_x->first. ' '. $svc_x->get('last');
+ $hash{'name'} = $svc_x->company. ' ('. $hash{'name'}. ')'
+ if $svc_x->company;
+ } else {
+ $hash{'name'} = $cust_main->name;
+ }
+ # no usage to hide here
+
+ } elsif ( $svcdb eq 'svc_phone' or $svcdb eq 'svc_pbx' ) {
+ if (!$hide_usage) {
+ # could potentially show lots of things...
+ $hash{'outbound'} = 1;
+ $hash{'inbound'} = 0;
+ if ( $svcdb eq 'svc_phone' ) {
+ if ( $part_pkg->plan eq 'voip_inbound' ) {
+ $hash{'outbound'} = 0;
+ $hash{'inbound'} = 1;
+ } elsif ( $part_pkg->option('selfservice_inbound_format')
+ or $conf->config('selfservice-default_inbound_cdr_format')
+ ) {
+ $hash{'inbound'} = 1;
+ }
+ }
+ foreach (qw(inbound outbound)) {
+ # hmm...we can't filter by status here, because there might
+ # not be cdr_terminations at all. have to go by date.
+ # find all since the last bill date.
+ # XXX cdr types? we are going to need them.
+ if ( $hash{$_} ) {
+ my $sum_cdr = $svc_x->sum_cdrs(
+ 'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
+ 'begin' => ($cust_pkg->last_bill || 0),
+ 'nonzero' => 1,
+ 'disable_charged_party' => 1,
+ );
+ $hash{$_} = $sum_cdr->hashref;
+ }
+ }
+ } # not hiding usage
+ } # svcdb
+
+ push @svcs, \%hash;
+ } # foreach $cust_svc
+
+ return {
'svcnum' => $session->{'svcnum'},
'custnum' => $custnum,
'date_format' => $conf->config('date_format') || '%m/%d/%Y',
'view_usage_nodomain' => $conf->exists('selfservice-view_usage_nodomain'),
- 'svcs' => [
- map {
- my $svc_x = $_->svc_x;
- my($label, $value) = $_->label;
- my $part_svc = $_->part_svc;
- my $svcdb = $part_svc->svcdb;
- my $cust_pkg = $_->cust_pkg;
- my $part_pkg = $cust_pkg->part_pkg;
-
- my %hash = (
- 'svcnum' => $_->svcnum,
- 'display_svcnum' => $_->display_svcnum,
- 'svcdb' => $svcdb,
- 'label' => $label,
- 'value' => $value,
- 'pkg_label' => $cust_pkg->pkg_locale,
- 'pkg_status' => $cust_pkg->status,
- 'readonly' => ($part_svc->selfservice_access eq 'readonly'),
- );
-
- if ( $svcdb eq 'svc_acct' ) {
- %hash = (
- %hash,
- 'username' => $svc_x->username,
- 'email' => $svc_x->email,
- 'finger' => $svc_x->finger,
- 'seconds' => $svc_x->seconds,
- 'upbytes' => display_bytecount($svc_x->upbytes),
- 'downbytes' => display_bytecount($svc_x->downbytes),
- 'totalbytes' => display_bytecount($svc_x->totalbytes),
-
- 'recharge_amount' => $part_pkg->option('recharge_amount',1),
- 'recharge_seconds' => $part_pkg->option('recharge_seconds',1),
- 'recharge_upbytes' =>
- display_bytecount($part_pkg->option('recharge_upbytes',1)),
- 'recharge_downbytes' =>
- display_bytecount($part_pkg->option('recharge_downbytes',1)),
- 'recharge_totalbytes' =>
- display_bytecount($part_pkg->option('recharge_totalbytes',1)),
- # more...
- );
-
- } elsif ( $svcdb eq 'svc_dsl' ) {
- $hash{'phonenum'} = $svc_x->phonenum;
- if ( $svc_x->first || $svc_x->get('last') || $svc_x->company ) {
- $hash{'name'} = $svc_x->first. ' '. $svc_x->get('last');
- $hash{'name'} = $svc_x->company. ' ('. $hash{'name'}. ')'
- if $svc_x->company;
- } else {
- $hash{'name'} = $cust_main->name;
- }
- } elsif ( $svcdb eq 'svc_phone' ) {
- # could potentially show lots of things...
- $hash{'outbound'} = 1;
- $hash{'inbound'} = 0;
- if ( $part_pkg->plan eq 'voip_inbound' ) {
- $hash{'outbound'} = 0;
- $hash{'inbound'} = 1;
- } elsif ( $part_pkg->option('selfservice_inbound_format')
- or $conf->config('selfservice-default_inbound_cdr_format')
- ) {
- $hash{'inbound'} = 1;
- }
- foreach (qw(inbound outbound)) {
- # hmm...we can't filter by status here, because there might
- # not be cdr_terminations at all. have to go by date.
- # find all since the last bill date.
- # XXX cdr types? we are going to need them.
- if ( $hash{$_} ) {
- my $sum_cdr = $svc_x->sum_cdrs(
- 'inbound' => ( $_ eq 'inbound' ? 1 : 0 ),
- 'begin' => ($cust_pkg->last_bill || 0),
- 'nonzero' => 1,
- );
- $hash{$_} = $sum_cdr->hashref;
- }
- }
- }
-
- # elsif ( $svcdb eq 'svc_phone' || $svcdb eq 'svc_port' ) {
- # %hash = (
- # %hash,
- # );
- #}
-
- \%hash;
- }
- @cust_svc
- ],
+ 'svcs' => \@svcs,
'usage_pools' => [
map { $usage_pools{$_} }
sort { $a cmp $b }
keys %usage_pools
],
+ 'hide_usage' => $hide_usage,
};
}
# XXX CDR type support...
# XXX any way to do a paged search on this?
# we have to return the results all at once...
- my($svc_phone, $begin, $end, %opt) = @_;
+ my($svc_x, $begin, $end, %opt) = @_;
map [ $_->downstream_csv(%opt, 'keeparray' => 1) ],
- $svc_phone->get_cdrs( 'begin'=>$begin, 'end'=>$end, %opt );
+ $svc_x->get_cdrs(
+ 'begin' => $begin,
+ 'end' => $end,
+ 'disable_charged_party' => 1,
+ %opt
+ );
}
sub list_cdr_usage {
my $p = shift;
- _usage_details( \&_list_cdr_usage, $p,
- 'svcdb' => 'svc_phone',
- );
+ _usage_details( \&_list_cdr_usage, $p );
}
sub _usage_details {
my($callback, $p, %opt) = @_;
+ my $conf = FS::Conf->new;
+
+ if ( $conf->exists('selfservice_hide-usage') ) {
+ return { 'error' => 'Viewing usage is not allowed.' };
+ }
my($context, $session, $custnum) = _custoragent_session_custnum($p);
return { 'error' => $session } if $context eq 'error';
my $search = { 'svcnum' => $p->{'svcnum'} };
$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
- my $svcdb = $opt{'svcdb'} || 'svc_acct';
-
- my $svc_x = qsearchs( $svcdb, $search );
+ my $cust_svc = qsearchs( 'cust_svc', $search );
return { 'error' => 'No service selected in list_svc_usage' }
- unless $svc_x;
+ unless $cust_svc;
- my $cust_pkg = $svc_x->cust_svc->cust_pkg;
+ my $svc_x = $cust_svc->svc_x;
+ my $svcdb = $svc_x->table;
+ my $cust_pkg = $cust_svc->cust_pkg;
my $freq = $cust_pkg->part_pkg->freq;
my %callback_opt;
my $header = [];
- if ( $svcdb eq 'svc_phone' ) {
- my $conf = FS::Conf->new;
+ if ( $svcdb eq 'svc_phone' or $svcdb eq 'svc_pbx' ) {
my $format = '';
if ( $p->{inbound} ) {
$format = $cust_pkg->part_pkg->option('selfservice_inbound_format')
%callback_opt
);
+ if ( $conf->exists('selfservice-hide_cdr_price') ) {
+ # ugly kludge, I know
+ my ($delete_col) = grep { $header->[$_] eq 'Price' } (0..scalar(@$header));
+ if (defined $delete_col) {
+ delete($_->[$delete_col]) foreach ($header, @usage);
+ }
+ }
+
#kinda false laziness with FS::cust_main::bill, but perhaps
#we should really change this bit to DateTime and DateTime::Duration
#
or return { 'error' => "unknown custnum $custnum" };
my $status = $cust_main->status;
+
+ my %order_pkg_options = ();
+ if ( $p->{locationnum} > 0 ) {
+ $order_pkg_options{locationnum} = delete($p->{locationnum});
+ } elsif ( $p->{address1} ) {
+ $order_pkg_options{'cust_location'} = new FS::cust_location {
+ map { $_ => $p->{$_} }
+ qw( address1 address2 city county state zip country )
+ };
+ }
+
#false laziness w/ClientAPI/Signup.pm
my $cust_pkg = new FS::cust_pkg ( {
- 'custnum' => $custnum,
- 'pkgpart' => $p->{'pkgpart'},
+ 'custnum' => $custnum,
+ 'pkgpart' => $p->{'pkgpart'},
+ 'quantity' => $p->{'quantity'} || 1,
} );
my $error = $cust_pkg->check;
return { 'error' => $error } if $error;
}
- use Tie::RefHash;
- tie my %hash, 'Tie::RefHash';
- %hash = ( $cust_pkg => \@svc );
- #msgcat
- $error = $cust_main->order_pkgs( \%hash, 'noexport' => 1 );
+ $error = $cust_main->order_pkg(
+ 'cust_pkg' => $cust_pkg,
+ 'svcs' => \@svc,
+ 'noexport' => 1,
+ %order_pkg_options,
+ );
return { 'error' => $error } if $error;
my $conf = new FS::Conf;
my($context, $session, $custnum) = _custoragent_session_custnum($p);
return { 'error' => $session } if $context eq 'error';
+ my $conf = new FS::Conf;
+ my $immutable = $conf->exists('selfservice_immutable-package');
+ return { 'error' => "Package modification disabled" } if $immutable;
+
my $search = { 'custnum' => $custnum };
$search->{'agentnum'} = $session->{'agentnum'} if $context eq 'agent';
my $cust_main = qsearchs('cust_main', $search )
my $cust_pkg = qsearchs('cust_pkg', { 'pkgnum' => $p->{pkgnum} } )
or return { 'error' => "unknown package $p->{pkgnum}" };
- my @newpkg;
- my $error = FS::cust_pkg::order( $custnum,
- [$p->{pkgpart}],
- [$p->{pkgnum}],
- \@newpkg,
- );
+ #if someone does need self-service package change of suspended packages,
+ # figure out how to be more discriminating
+ return { error=>"Can't change a suspended package", pkgnum=>$cust_pkg->pkgnum}
+ if $cust_pkg->status eq 'suspended';
+
+ my $err_or_cust_pkg = $cust_pkg->change( 'pkgpart' => $p->{'pkgpart'},
+ 'quantity' => $p->{'quantity'} || 1,
+ );
+
+ return { error=>$err_or_cust_pkg, pkgnum=>$cust_pkg->pkgnum }
+ unless ref($err_or_cust_pkg);
- my $conf = new FS::Conf;
if ( $conf->exists('signup_server-realtime') ) {
- my $bill_error = _do_bop_realtime( $cust_main, $status );
+ my $bill_error = _do_bop_realtime( $cust_main, $status, 'no_invoice_void'=>1 );
if ($bill_error) {
- $newpkg[0]->suspend;
+ $err_or_cust_pkg->suspend;
return $bill_error;
} else {
- $newpkg[0]->reexport;
+ $err_or_cust_pkg->reexport;
}
} else {
- $newpkg[0]->reexport;
+ $err_or_cust_pkg->reexport;
}
return { error => '', pkgnum => $cust_pkg->pkgnum };
}
sub _do_bop_realtime {
- my ($cust_main, $status) = (shift, shift);
+ my ($cust_main, $status, %opt) = @_;
my $old_balance = $cust_main->balance;
- my $bill_error = $cust_main->bill
- || $cust_main->apply_payments_and_credits
- || $cust_main->realtime_collect('selfservice' => 1);
+ my @cust_bill;
+ my $bill_error = $cust_main->bill(
+ 'return_bill' => \@cust_bill,
+ );
+
+ $bill_error ||= $cust_main->apply_payments_and_credits;
+
+ $bill_error ||= $cust_main->realtime_collect('selfservice' => 1)
+ if $cust_main->payby =~ /^(CARD|CHEK)$/;
if ( $cust_main->balance > $old_balance
&& $cust_main->balance > 0
- && ( $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/ ?
- 1 : $status eq 'suspended' ) ) {
- #this makes sense. credit is "un-doing" the invoice
- my $conf = new FS::Conf;
- $cust_main->credit( sprintf("%.2f", $cust_main->balance - $old_balance ),
- 'self-service decline',
- 'reason_type' => $conf->config('signup_credit_type'),
- );
- $cust_main->apply_credits( 'order' => 'newest' );
+ && ( $cust_main->payby !~ /^(BILL|DCRD|DCHK)$/
+ || $status eq 'suspended'
+ )
+ )
+ {
+ unless ( $opt{'no_invoice_void'} ) {
+
+ #this used to apply a credit, but now we can void invoices...
+ foreach my $cust_bill (@cust_bill) {
+ my $voiderror = $cust_bill->void('automatic payment failed');
+ warn "Error voiding cust bill after decline: $voiderror" if $voiderror;
+ }
+
+ }
return { 'error' => '_decline', 'bill_error' => $bill_error };
}
}
sub provision_phone {
- my $p = shift;
- my @bulkdid;
- @bulkdid = @{$p->{'bulkdid'}} if $p->{'bulkdid'};
+ my $p = shift;
+ my @bulkdid;
+ @bulkdid = @{$p->{'bulkdid'}} if $p->{'bulkdid'};
- if($p->{'svcnum'} && $p->{'svcnum'} =~ /^\d+$/){
+ #editing an existing phone number
+ if ( $p->{'svcnum'} && $p->{'svcnum'} =~ /^\d+$/ ) {
my($context, $session, $custnum) = _custoragent_session_custnum($p);
return { 'error' => $session } if $context eq 'error';
return { 'error' => $svc_phone->replace };
}
-# single DID LNP
- unless($p->{'lnp'}) {
+ # single DID LNP
+ unless ( $p->{'lnp'} ) {
$p->{'lnp_desired_due_date'} = parse_datetime($p->{'lnp_desired_due_date'});
$p->{'lnp_status'} = "portingin";
return _provision( 'FS::svc_phone',
$p,
@_
);
- }
+ }
-# single DID order
- unless (scalar(@bulkdid)) {
+ # single DID order (the usual case)
+ unless (scalar(@bulkdid)) {
return _provision( 'FS::svc_phone',
[qw(phonenum countrycode)],
[qw(phonenum countrycode)],
$p,
@_
);
- }
+ }
-# bulk DID order case
+ # bulk DID order case
my $error;
foreach my $did ( @bulkdid ) {
$did =~ s/[^0-9]//g;
{ 'bulkdid' => [ @bulkdid ], 'svc' => $error->{'svc'} }
}
+sub provision_pbx {
+ my $p = shift;
+ warn "provision_pbx called\n"
+ if $DEBUG;
+
+ warn "provision_pbx calling _provision\n"
+ if $DEBUG;
+ _provision( 'FS::svc_pbx',
+ [qw(id title max_extensions max_simultaneous ip_addr)],
+ [qw(id title max_extensions max_simultaneous ip_addr)],
+ $p,
+ @_
+ );
+}
+
sub provision_acct {
my $p = shift;
warn "provision_acct called\n"
);
}
+sub provision_forward {
+ my $p = shift;
+ _provision( 'FS::svc_forward',
+ ['srcsvc','src','dstsvc','dst'],
+ [],
+ $p,
+ );
+}
+
sub _provision {
my( $class, $fields, $return_fields, $p ) = splice(@_, 0, 4);
warn "_provision called for $class\n"
my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } )
or return { 'error' => "unknown svcpart $p->{'svcpart'}" };
+ return { error=> 'svcpart '. $p->{'svcpart'}. " is not a $class definition" }
+ if $class ne 'FS::'. $part_svc->svcdb;
+
warn "creating $class record\n"
if $DEBUG;
my $svc_x = $class->new( {
'svcpart' => $p->{'svcpart'},
map { $_ => $p->{$_} } @$fields
} );
+
+ my %insert_args = ();
+ #i shouldn't be a special case here (pass an option or something)
+ if ( $class eq 'FS::svc_phone'
+ && grep length($p->{$_}), @location_editable_fields
+ )
+ {
+ $insert_args{'cust_location'} = new FS::cust_location {
+ map { $_ => $p->{$_} } @location_editable_fields
+ };
+ }
+
warn "inserting $class record\n"
if $DEBUG;
- my $error = $svc_x->insert;
+ my $error = $svc_x->insert(%insert_args);
unless ( $error ) {
warn "finding inserted record for svcnum ". $svc_x->svcnum. "\n"
}
}
+ if ($ret->{'svcdb'} eq 'svc_forward') {
+ $ret->{'forward_emails'} = {$cust_pkg->forward_emails()};
+ }
+
$ret;
}
} )
or return { 'error' => "Service not found" };
- if ( exists($p->{'old_password'}) ) {
- return { 'error' => "Incorrect password." }
- unless $svc_acct->check_password($p->{'old_password'});
- }
+ my $error = '';
- $svc_acct->set_password($p->{'new_password'});
- my $error = $svc_acct->replace();
+ my $conf = new FS::Conf;
+
+ return { 'error' => 'Incorrect current password.' }
+ if ( exists($p->{'old_password'})
+ || $conf->exists('selfservice-password_change_oldpass')
+ )
+ && ! $svc_acct->check_password($p->{'old_password'});
+
+ # should move password length checks into is_password_allowed
+ $error = 'Password too short.'
+ if length($p->{'new_password'}) < ($conf->config('passwordmin') || 6);
+ $error = 'Password too long.'
+ if length($p->{'new_password'}) > ($conf->config('passwordmax') || 8);
+
+ $error ||= $svc_acct->is_password_allowed($p->{'new_password'})
+ || $svc_acct->set_password($p->{'new_password'})
+ || $svc_acct->replace();
+
+ #regular pw change in self-service should change contact pw too, otherwise its
+ #way too confusing. hell its confusing they're separate at all, but alas.
+ #need to support the "ISP provides email that's used as a contact email" case
+ #as well as we can.
+ my $contact = FS::contact->by_selfservice_email($svc_acct->email);
+ if ( $contact && qsearchs('cust_contact', { contactnum=> $contact->contactnum,
+ custnum => $custnum,
+ selfservice_access => 'Y',
+ }
+ )
+ ) {
+ #svc_acct was successful but this one returns an error? "shouldn't happen"
+ $error ||= $contact->change_password($p->{'new_password'});
+ }
my($label, $value) = $svc_acct->cust_svc->label;
sub reset_passwd {
my $p = shift;
+ my $info = skin_info($p);
+
my $conf = new FS::Conf;
my $verification = $conf->config('selfservice-password_reset_verification')
- or return { 'error' => 'Password resets disabled' };
+ or return { %$info, 'error' => 'Password resets disabled' };
+
+ my $contact = '';
+ my $svc_acct = '';
+ my $cust_main = '';
+ if ( $p->{'email'} ) { #new-style, changes contact and svc_acct
+
+ $contact = FS::contact->by_selfservice_email($p->{'email'});
- my $username = $p->{'username'};
+ if ( $contact ) {
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $cust_main = $cust_contact[0]->cust_main if scalar(@cust_contact) == 1;
+ }
+
+ #also look for an svc_acct, otherwise it would be super confusing
+
+ my($username, $domain) = split('@', $p->{'email'});
+ my $svc_domain = qsearchs('svc_domain', { 'domain' => $domain } );
+ if ( $svc_domain ) {
+ $svc_acct = qsearchs('svc_acct', { 'username' => $username,
+ 'domsvc' => $svc_domain->svcnum }
+ );
+ if ( $svc_acct ) {
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg;
+ $cust_main ||= $cust_pkg->cust_main if $cust_pkg;
+
+ #precaution: don't change svc_acct password not part of the same
+ # customer as contact
+ $svc_acct = '' if ! $cust_pkg
+ || $cust_pkg->custnum != $cust_main->custnum;
+ }
+
+ }
- my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
- or return { 'error' => 'Account not found' };
+ return { %$info, 'error' => 'Email address not found' }
+ unless $contact || $svc_acct;
+
+ } elsif ( $p->{'username'} ) { #old style, looks in svc_acct only
+
+ my $svc_domain = qsearchs('svc_domain', { 'domain' => $p->{'domain'} } )
+ or return { %$info, 'error' => 'Account not found' };
- my $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
- 'domsvc' => $svc_domain->svcnum }
- )
- or return { 'error' => 'Account not found' };
+ $svc_acct = qsearchs('svc_acct', { 'username' => $p->{'username'},
+ 'domsvc' => $svc_domain->svcnum }
+ )
+ or return { %$info, 'error' => 'Account not found' };
- my $cust_pkg = $svc_acct->cust_svc->cust_pkg
- or return { 'error' => 'Account not found' };
+ my $cust_pkg = $svc_acct->cust_svc->cust_pkg
+ or return { %$info, 'error' => 'Account not found' };
- my $cust_main = $cust_pkg->cust_main;
+ $cust_main = $cust_pkg->cust_main;
+
+ }
+
+ return { %$info, 'error' => 'Multi-customer contacts incompatible with customer-based verification' }
+ if ! $cust_main && $verification ne 'email';
my %verify = (
+ 'email' => sub { 1; },
'paymask' => sub {
my( $p, $cust_main ) = @_;
$cust_main->payby =~ /^(CARD|DCRD|CHEK|DCHK)$/
foreach my $verify ( split(',', $verification) ) {
&{ $verify{$verify} }( $p, $cust_main )
- or return { 'error' => 'Account not found' };
+ or return { %$info, 'error' => 'Account not found' };
}
- #okay, we're verified, now create a unique session
+ #okay, we're verified
- my $reset_session = {
- 'svcnum' => $svc_acct->svcnum,
- };
+ if ( $contact ) {
- my $timeout = '1 hour'; #?
+ my $error = $contact->send_reset_email(
+ 'svcnum' => ($svc_acct ? $svc_acct->svcnum : ''),
+ );
- my $reset_session_id;
- do {
- $reset_session_id = md5_hex(md5_hex(time(). {}. rand(). $$))
- } until ( ! defined _cache->get("reset_passwd_$reset_session_id") ); #just in case
+ if ( $error ) {
+ return { %$info, 'error' => $error }; #????
+ }
+
+ } elsif ( $svc_acct ) {
- _cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
+ #create a unique session
- #email it
+ my $reset_session = {
+ 'svcnum' => $svc_acct->svcnum,
+ 'agentnum' => $svc_acct->cust_main->agentnum,
+ };
+
+ my $timeout = '1 hour'; #?
+
+ my $reset_session_id;
+ do {
+ $reset_session_id = sha512_hex(time(). {}. rand(). $$)
+ } until ( ! defined _cache->get("reset_passwd_$reset_session_id") );
+ #just in case
+
+ _cache->set( "reset_passwd_$reset_session_id", $reset_session, $timeout );
+
+ #email it
+
+ my $msgnum = $conf->config('selfservice-password_reset_msgnum',
+ $cust_main->agentnum);
+ #die "selfservice-password_reset_msgnum unset" unless $msgnum;
+ return { %$info, 'error' => "selfservice-password_reset_msgnum unset" }
+ unless $msgnum;
+ my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
+ my $error = $msg_template->send( 'cust_main' => $cust_main,
+ 'object' => $svc_acct,
+ 'substitutions' => {
+ 'session_id' => $reset_session_id,
+ }
+ );
+ if ( $error ) {
+ return { %$info, 'error' => $error }; #????
+ }
- my $msgnum = $conf->config('selfservice-password_reset_msgnum', $cust_main->agentnum);
- #die "selfservice-password_reset_msgnum unset" unless $msgnum;
- return { 'error' => "selfservice-password_reset_msgnum unset" } unless $msgnum;
- my $msg_template = qsearchs('msg_template', { msgnum => $msgnum } );
- my $error = $msg_template->send( 'cust_main' => $cust_main,
- 'object' => $svc_acct,
- 'substitutions' => {
- 'session_id' => $reset_session_id,
- }
- );
- if ( $error ) {
- return { 'error' => $error }; #????
}
- return { 'error' => '' };
+ return { %$info, 'error' => '' };
}
sub check_reset_passwd {
my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
or return { 'error' => "Can't resume session" }; #better error message
- my $svcnum = $reset_session->{'svcnum'};
+ if ( $reset_session->{'svcnum'} ) {
- my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
- or return { 'error' => "Service not found" };
+ my $svcnum = $reset_session->{'svcnum'};
- return { 'error' => '',
- 'username' => $svc_acct->username,
- };
+ my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+ or return { 'error' => "Service not found" };
+
+ $p->{'agentnum'} = $svc_acct->cust_svc->cust_pkg->cust_main->agentnum;
+ my $info = skin_info($p);
+
+ return { %$info,
+ 'error' => '',
+ 'session_id' => $p->{'session_id'},
+ 'username' => $svc_acct->username,
+ };
+
+ } elsif ( $reset_session->{'contactnum'} ) {
+
+ my $contactnum = $reset_session->{'contactnum'};
+
+ my $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+ or return { 'error' => "Contact not found" };
+
+ my @contact_email = $contact->contact_email;
+ return { 'error' => 'No contact email' } unless @contact_email;
+
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+ if scalar(@cust_contact) == 1;
+ my $info = skin_info($p);
+
+ return { %$info,
+ 'error' => '',
+ 'session_id' => $p->{'session_id'},
+ 'email' => $contact_email[0]->email, #the first?
+ };
+
+ } else {
+
+ return { 'error' => 'No svcnum or contactnum in session' }; #??
+
+ }
}
my $verification = $conf->config('selfservice-password_reset_verification')
or return { 'error' => 'Password resets disabled' };
- return { 'error' => "New passwords don't match." }
+ my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
+ or return { 'error' => "Can't resume session" }; #better error message
+
+ my $info = '';
+
+ my $svc_acct = '';
+ if ( $reset_session->{'svcnum'} ) {
+
+ my $svcnum = $reset_session->{'svcnum'};
+
+ $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
+ or return { 'error' => "Service not found" };
+
+ $p->{'agentnum'} ||= $svc_acct->cust_svc->cust_pkg->cust_main->agentnum;
+ $info ||= skin_info($p);
+
+ }
+
+ my $contact = '';
+ if ( $reset_session->{'contactnum'} ) {
+
+ my $contactnum = $reset_session->{'contactnum'};
+
+ $contact = qsearchs('contact', { 'contactnum' => $contactnum } )
+ or return { 'error' => "Contact not found" };
+
+ my @cust_contact = grep $_->selfservice_access, $contact->cust_contact;
+ $p->{'agentnum'} = $cust_contact[0]->cust_main->agentnum
+ if scalar(@cust_contact) == 1;
+ $info ||= skin_info($p);
+
+ }
+
+ return { %$info, 'error' => "New passwords don't match." }
if $p->{'new_password'} ne $p->{'new_password2'};
- return { 'error' => 'Enter new password' }
+ return { %$info, 'error' => 'Enter new password' }
unless length($p->{'new_password'});
- my $reset_session = _cache->get('reset_passwd_'. $p->{'session_id'})
- or return { 'error' => "Can't resume session" }; #better error message
+ if ( $svc_acct ) {
- my $svcnum = $reset_session->{'svcnum'};
+ my $error ||= $svc_acct->is_password_allowed($p->{'new_password'})
+ || $svc_acct->set_password($p->{'new_password'})
+ || $svc_acct->replace();
- my $svc_acct = qsearchs('svc_acct', { 'svcnum' => $svcnum } )
- or return { 'error' => "Service not found" };
+ return { %$info, 'error' => $error } if $error;
- $svc_acct->set_password($p->{'new_password'});
- my $error = $svc_acct->replace();
+ #my($label, $value) = $svc_acct->cust_svc->label;
+ #return { 'error' => $error,
+ # #'label' => $label,
+ # #'value' => $value,
+ # };
- my($label, $value) = $svc_acct->cust_svc->label;
+ }
- return { 'error' => $error,
- #'label' => $label,
- #'value' => $value,
- };
+ if ( $contact ) {
+
+ my $error = $contact->change_password($p->{'new_password'});
+
+ return { %$info, 'error' => $error }; # if $error;
+
+ }
+
+ #password changed ,so remove session, don't want it reused
+ _cache->remove($p->{'session_id'});
+
+ return { %$info, 'error' => '' };
+
+}
+
+sub list_tickets {
+ my $p = shift;
+ my($context, $session, $custnum) = _custoragent_session_custnum($p);
+ return { 'error' => $session } if $context eq 'error';
+
+ my @tickets = ();
+ if ( $session->{'pkgnum'} ) {
+
+ #tickets for specific service with pkg-balances on
+ my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum,
+ 'pkgnum' => $session->{'pkgnum'} })
+ or return { 'error' => 'unknown package' };
+ foreach my $cust_svc ( $cust_pkg->cust_svc ) {
+ push @tickets, $cust_svc->tickets( $p->{status} );
+ }
+
+ } else {
+
+ my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } )
+ or return { 'error' => "unknown custnum $custnum" };
+
+ @tickets = $cust_main->tickets( $p->{status} );
+ }
+
+ # unavoidable false laziness w/ httemplate/view/cust_main/tickets.html
+ if ( $FS::TicketSystem::system && FS::TicketSystem->selfservice_priority ) {
+ my $conf = new FS::Conf;
+ my $dir = $conf->exists('ticket_system-priority_reverse') ? -1 : 1;
+ +{ tickets => [
+ sort {
+ (
+ ($a->{'_selfservice_priority'} eq '') <=>
+ ($b->{'_selfservice_priority'} eq '')
+ ) ||
+ ( $dir *
+ ($b->{'_selfservice_priority'} <=> $a->{'_selfservice_priority'})
+ )
+ } @tickets
+ ]
+ };
+ } else {
+ +{ tickets => \@tickets };
+ }
}
my($context, $session, $custnum) = _custoragent_session_custnum($p);
return { 'error' => $session } if $context eq 'error';
- warn "$me get_ticket: initializing ticket system\n" if $DEBUG;
- FS::TicketSystem->init();
- return { 'error' => 'get_ticket configuration error' }
- if $FS::TicketSystem::system ne 'RT_Internal';
+# warn "$me get_ticket: initializing ticket system\n" if $DEBUG;
+# FS::TicketSystem->init();
+# return { 'error' => 'get_ticket configuration error' }
+# if $FS::TicketSystem::system ne 'RT_Internal';
# check existence and ownership as part of this
warn "$me get_ticket: fetching ticket\n" if $DEBUG;
my($context, $session, $custnum) = _custoragent_session_custnum($p);
return { 'error' => $session } if $context eq 'error';
- warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG;
- FS::TicketSystem->init;
+# warn "$me adjust_ticket_priority: initializing ticket system\n" if $DEBUG;
+# FS::TicketSystem->init;
my $ss_priority = FS::TicketSystem->selfservice_priority;
return { 'error' => 'adjust_ticket_priority configuration error' }