From: ivan Date: Thu, 15 Jul 2004 22:40:01 +0000 (+0000) Subject: big update for customer self-service: add provisioning/unprovisioning of purchased... X-Git-Tag: BEFORE_FINAL_MASONIZE~980 X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=6ced9264b8ec79e4b460be90ede25ec72a7dfc16;ds=sidebyside big update for customer self-service: add provisioning/unprovisioning of purchased services, like fs_selfadmin --- diff --git a/FS/FS/ClientAPI/MyAccount.pm b/FS/FS/ClientAPI/MyAccount.pm index f51174e5d..639cb7b9b 100644 --- a/FS/FS/ClientAPI/MyAccount.pm +++ b/FS/FS/ClientAPI/MyAccount.pm @@ -22,6 +22,7 @@ use FS::cust_pkg; use FS::ClientAPI; #hmm FS::ClientAPI->register_handlers( 'MyAccount/login' => \&login, + 'MyAccount/logout' => \&logout, 'MyAccount/customer_info' => \&customer_info, 'MyAccount/edit_info' => \&edit_info, 'MyAccount/invoice' => \&invoice, @@ -33,6 +34,9 @@ FS::ClientAPI->register_handlers( 'MyAccount/order_pkg' => \&order_pkg, 'MyAccount/cancel_pkg' => \&cancel_pkg, 'MyAccount/charge' => \&charge, + 'MyAccount/part_svc_info' => \&part_svc_info, + 'MyAccount/provision_acct' => \&provision_acct, + 'MyAccount/unprovision_svc' => \&unprovision_svc, ); use vars qw( @cust_main_editable_fields ); @@ -92,6 +96,16 @@ sub login { }; } +sub logout { + my $p = shift; + if ( $p->{'session_id'} ) { + $cache->remove($p->{'session_id'}); + return { 'error' => '' }; + } else { + return { 'error' => "Can't resume session" }; #better error message + } +} + sub customer_info { my $p = shift; @@ -445,7 +459,24 @@ sub list_pkgs { my $cust_main = qsearchs('cust_main', { 'custnum' => $custnum } ) or return { 'error' => "unknown custnum $custnum" }; - return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] }; + #return { 'cust_pkg' => [ map { $_->hashref } $cust_main->ncancelled_pkgs ] }; + + { 'svcnum' => $session->{'svcnum'}, + 'cust_pkg' => [ map { + { $_->hash, + $_->part_pkg->hash, + part_svc => + [ map $_->hashref, $_->available_part_svc ], + cust_svc => + [ map { { $_->hash, + label => [ $_->label ], + } + } $_->cust_svc + ], + }; + } $cust_main->ncancelled_pkgs + ], + }; } @@ -598,5 +629,110 @@ sub cancel_pkg { } +sub provision_acct { + 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 = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $pkgnum = $p->{'pkgnum'}; + + my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum, + 'pkgnum' => $pkgnum, + } ) + or return { 'error' => "unknown pkgnum $pkgnum" }; + + my $part_svc = qsearchs('part_svc', { 'svcpart' => $p->{'svcpart'} } ) + or return { 'error' => "unknown svcpart $p->{'svcpart'}" }; + + return { 'error' => gettext('passwords_dont_match') } + if $p->{'_password'} ne $p->{'_password2'}; + return { 'error' => gettext('empty_password') } + unless length($p->{'_password'}); + + my $svc_acct = new FS::svc_acct( { + 'pkgnum' => $p->{'pkgnum'}, + 'svcpart' => $p->{'svcpart'}, + 'username' => $p->{'username'}, + '_password' => $p->{'_password'}, + } ); + + return { 'svc' => $part_svc->svc, + 'error' => $svc_acct->insert + }; + +} + +sub part_svc_info { + 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 = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $pkgnum = $p->{'pkgnum'}; + + my $cust_pkg = qsearchs('cust_pkg', { 'custnum' => $custnum, + 'pkgnum' => $pkgnum, + } ) + or return { 'error' => "unknown pkgnum $pkgnum" }; + + my $svcpart = $p->{'svcpart'}; + + my $pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $cust_pkg->pkgpart, + 'svcpart' => $svcpart, } ) + or return { 'error' => "unknown svcpart $svcpart for pkgnum $pkgnum" }; + my $part_svc = $pkg_svc->part_svc; + + return { + 'svc' => $part_svc->svc, + 'svcdb' => $part_svc->svcdb, + 'pkgnum' => $pkgnum, + 'svcpart' => $svcpart, + + 'security_phrase' => 0, #XXX ! + 'svc_acct_pop' => [], #XXX ! + 'popnum' => '', + 'init_popstate' => '', + 'popac' => '', + 'acstate' => '', + }; + +} + +sub unprovision_svc { + 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 = qsearchs('cust_main', { 'custnum' => $custnum } ) + or return { 'error' => "unknown custnum $custnum" }; + + my $svcnum = $p->{'svcnum'}; + + my $cust_svc = qsearchs('cust_svc', { 'svcnum' => $svcnum, } ) + or return { 'error' => "unknown svcnum $svcnum" }; + + return { 'error' => "Service $svcnum does not belong to customer $custnum" } + unless $cust_svc->cust_pkg->custnum == $custnum; + + return { 'svc' => $cust_svc->part_svc->svc, + 'error' => $cust_svc->cancel + }; + +} + 1; diff --git a/FS/FS/ClientAPI/Signup.pm b/FS/FS/ClientAPI/Signup.pm index 2e2b80fcb..81ed5e65c 100644 --- a/FS/FS/ClientAPI/Signup.pm +++ b/FS/FS/ClientAPI/Signup.pm @@ -128,7 +128,7 @@ sub new_customer { #return "Passwords don't match" # if $hashref->{'_password'} ne $hashref->{'_password2'} return { 'error' => gettext('empty_password') } - unless $packet->{'_password'}; + unless length($packet->{'_password'}); # a bit inefficient for large numbers of pops return { 'error' => gettext('no_access_number_selected') } unless $packet->{'popnum'} || !scalar(qsearch('svc_acct_pop',{} )); diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index fb41dfcf8..d2a48e9f7 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -502,15 +502,22 @@ sub part_pkg { : qsearchs( 'part_pkg', { 'pkgpart' => $self->pkgpart } ); } -=item cust_svc +=item cust_svc [ SVCPART ] Returns the services for this package, as FS::cust_svc objects (see -L) +L). If a svcpart is specified, return only the matching +services. =cut sub cust_svc { my $self = shift; + + if ( @_ ) { + return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum, + 'svcpart' => shift, } ); + } + #if ( $self->{'_svcnum'} ) { # values %{ $self->{'_svcnum'}->cache }; #} else { @@ -524,8 +531,45 @@ sub cust_svc { $pkg_svc ? $pkg_svc->quantity : 0, ]; } - qsearch ( 'cust_svc', { 'pkgnum' => $self->pkgnum } ); + qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ); #} + +} + +=item num_cust_svc [ SVCPART ] + +Returns the number of provisioned services for this package. If a svcpart is +specified, counts only the matching services. + +=cut + +sub num_cust_svc { + my $self = shift; + my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?'; + $sql .= ' AND svcpart = ?' if @_; + my $sth = dbh->prepare($sql) or die dbh->errstr; + $sth->execute($self->pkgnum, @_) or die $sth->errstr; + $sth->fetchrow_arrayref->[0]; +} + +=item available_part_svc + +Returns a list FS::part_svc objects representing services included in this +package but not yet provisioned. Each FS::part_svc object also has an extra +field, I, which specifies the number of available services. + +=cut + +sub available_part_svc { + my $self = shift; + grep { $_->num_avail > 0 } + map { + my $part_svc = $_->part_svc; + $part_svc->{'Hash'}{'num_avail'} = #evil encapsulation-breaking + $_->quantity - $self->num_cust_svc($_->svcpart); + $part_svc; + } + $self->part_pkg->pkg_svc; } =item labels diff --git a/fs_selfservice/FS-SelfService/SelfService.pm b/fs_selfservice/FS-SelfService/SelfService.pm index 2cda9fe2d..ae6d37671 100644 --- a/fs_selfservice/FS-SelfService/SelfService.pm +++ b/fs_selfservice/FS-SelfService/SelfService.pm @@ -22,17 +22,21 @@ $socket .= '.'.$tag if defined $tag && length($tag); 'chfn' => 'passwd/passwd', 'chsh' => 'passwd/passwd', 'login' => 'MyAccount/login', + 'logout' => 'MyAccount/logout', 'customer_info' => 'MyAccount/customer_info', - 'edit_info' => 'MyAccount/edit_info', + 'edit_info' => 'MyAccount/edit_info', #add to ss cgi! 'invoice' => 'MyAccount/invoice', - 'list_invoices' => 'MyAccount/list_invoices', - 'cancel' => 'MyAccount/cancel', + 'list_invoices' => 'MyAccount/list_invoices', #? + 'cancel' => 'MyAccount/cancel', #add to ss cgi! 'payment_info' => 'MyAccount/payment_info', 'process_payment' => 'MyAccount/process_payment', - 'list_pkgs' => 'MyAccount/list_pkgs', - 'order_pkg' => 'MyAccount/order_pkg', - 'cancel_pkg' => 'MyAccount/cancel_pkg', - 'charge' => 'MyAccount/charge', + 'list_pkgs' => 'MyAccount/list_pkgs', #add to ss cgi! + 'order_pkg' => 'MyAccount/order_pkg', #add to ss cgi! + 'cancel_pkg' => 'MyAccount/cancel_pkg', #add to ss cgi! + 'charge' => 'MyAccount/charge', #? + 'part_svc_info' => 'MyAccount/part_svc_info', + 'provision_acct' => 'MyAccount/provision_acct', + 'unprovision_svc' => 'MyAccount/unprovision_svc', 'signup_info' => 'Signup/signup_info', 'new_customer' => 'Signup/new_customer', 'agent_login' => 'Agent/agent_login', @@ -466,9 +470,28 @@ Returns a hash reference containing customer package information. The hash refe =over 4 + =item cust_pkg HASHREF -Array reference of hash references, each of which has the fields of a cust_pkg record (see L). Note these are not FS::cust_pkg objects, but hash references of columns and values. +Array reference of hash references, each of which has the fields of a cust_pkg +record (see L) as well as the fields below. Note these are not +the internal FS:: objects, but hash references of columns and values. + +=item all fields of part_pkg (XXXpare this down to a secure subset) + +=item part_svc - An array of hash references, each of which has the following keys: + +=over 4 + +=item all fields of part_svc (XXXpare this down to a secure subset) + +=item avail + +=back + +=item error + +Empty on success, or an error message on errors. =back @@ -1033,7 +1056,8 @@ END =head1 RESELLER FUNCTIONS Note: Resellers can also use the B and B functions -with their active session. +with their active session, and the B and B functions +with their active session and an additonal I parameter. =over 4 diff --git a/fs_selfservice/FS-SelfService/cgi/agent.cgi b/fs_selfservice/FS-SelfService/cgi/agent.cgi index 2d948e790..6d2fd5840 100644 --- a/fs_selfservice/FS-SelfService/cgi/agent.cgi +++ b/fs_selfservice/FS-SelfService/cgi/agent.cgi @@ -221,6 +221,10 @@ sub process_order_pkg { my $results = ''; + unless ( length($cgi->param('_password')) ) { + my $init_data = signup_info( 'session_id' => $session_id ); + $results = { 'error' => $init_data->{msgcat}{empty_password} } + } if ( $cgi->param('_password') ne $cgi->param('_password2') ) { my $init_data = signup_info( 'session_id' => $session_id ); $results = { error => $init_data->{msgcat}{passwords_dont_match} }; diff --git a/fs_selfservice/FS-SelfService/cgi/delete_svc.html b/fs_selfservice/FS-SelfService/cgi/delete_svc.html new file mode 100644 index 000000000..16054a77c --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/delete_svc.html @@ -0,0 +1,18 @@ +MyAccount +MyAccount

+<%= $url = "$selfurl?session=$session_id;action="; ''; %> + +<%= include('myaccount_menu') %> +
+ +<%= if ( $error ) { + $OUT .= qq!Error: $error!; +} else { + $OUT .= "$svc removed."; +} %> + +
+
+powered by freeside + + diff --git a/fs_selfservice/FS-SelfService/cgi/logout.html b/fs_selfservice/FS-SelfService/cgi/logout.html new file mode 100644 index 000000000..0e774e9eb --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/logout.html @@ -0,0 +1,5 @@ +MyAccount +MyAccount

+You have been logged out. + + diff --git a/fs_selfservice/FS-SelfService/cgi/make_payment.html b/fs_selfservice/FS-SelfService/cgi/make_payment.html index cf6d62e22..3522c0867 100644 --- a/fs_selfservice/FS-SelfService/cgi/make_payment.html +++ b/fs_selfservice/FS-SelfService/cgi/make_payment.html @@ -1,10 +1,9 @@ MyAccount MyAccount

<%= $url = "$selfurl?session=$session_id;action="; ''; %> -
-MyAccount
- -
+ +<%= include('myaccount_menu') %> +
Make a payment

diff --git a/fs_selfservice/FS-SelfService/cgi/myaccount.html b/fs_selfservice/FS-SelfService/cgi/myaccount.html index f48fdedea..9997d7059 100644 --- a/fs_selfservice/FS-SelfService/cgi/myaccount.html +++ b/fs_selfservice/FS-SelfService/cgi/myaccount.html @@ -1,10 +1,9 @@ MyAccount MyAccount

<%= $url = "$selfurl?session=$session_id;action="; ''; %> -
-MyAccount
- -
+ +<%= include('myaccount_menu') %> +
Hello <%= $name %>!

<%= $small_custview %> @@ -15,7 +14,7 @@ Hello <%= $name %>!

<%= if ( @open_invoices ) { $OUT .= ''. - ''; my $link = qq! + diff --git a/fs_selfservice/FS-SelfService/cgi/payment_results.html b/fs_selfservice/FS-SelfService/cgi/payment_results.html index 92c8cf51b..44289deba 100644 --- a/fs_selfservice/FS-SelfService/cgi/payment_results.html +++ b/fs_selfservice/FS-SelfService/cgi/payment_results.html @@ -1,10 +1,9 @@ MyAccount MyAccount

<%= $url = "$selfurl?session=$session_id;action="; ''; %> -
Open Invoices'; + '
Open Invoices
+ +Overview

+Change payment info *

+Change service address *

+Setup my services

+Purchase additional package *

+ +Change password(s)

+Logout

+* coming soon +
-MyAccount
- -
+ +<%= include('myaccount_menu') %> +
Payment results

<%= if ( $error ) { $OUT .= qq!Error processing your payment: $error!; diff --git a/fs_selfservice/FS-SelfService/cgi/process_svc_acct.html b/fs_selfservice/FS-SelfService/cgi/process_svc_acct.html new file mode 100644 index 000000000..7052059c4 --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/process_svc_acct.html @@ -0,0 +1,14 @@ +MyAccount +MyAccount

+<%= $url = "$selfurl?session=$session_id;action="; ''; %> + +<%= include('myaccount_menu') %> +
+ +<%= $svc %> setup sucessfully. + +
+
+powered by freeside + + diff --git a/fs_selfservice/FS-SelfService/cgi/provision.html b/fs_selfservice/FS-SelfService/cgi/provision.html new file mode 100644 index 000000000..326f90229 --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/provision.html @@ -0,0 +1,77 @@ +MyAccount +MyAccount

+<%= $url = "$selfurl?session=$session_id;action="; ''; %> + +<%= include('myaccount_menu') %> +
+Setup services

+ + + +<%= foreach my $pkg ( + grep { scalar(@{$_->{part_svc}}) + || scalar(@{$_->{cust_svc}}) + } @cust_pkg + ) { + + $OUT .= ''. + ''; + + my $col1 = "ffffff"; + my $col2 = "dddddd"; + my $col = $col1; + + foreach my $cust_svc ( @{ $pkg->{cust_svc} } ) { + my $td = qq!'. + "$td>". $cust_svc->{label}[1]. ''. + "$td>"; + + #if ( $cust_svc->{label}[2] eq 'svc_acct' ) { + # $OUT .= qq!(!. + # 'change pw) '; + #} + + unless ( $cust_svc->{'svcnum'} == $svcnum ) { + $OUT .= qq!(!. + 'delete)'; + + } + $OUT .= ''; + $col = $col eq $col1 ? $col2 : $col1; + } + + $OUT .= '' + if scalar(@{$pkg->{part_svc}}) && scalar(@{$pkg->{cust_svc}}); + + my $col = $col1; + + foreach my $part_svc ( @{ $pkg->{part_svc} } ) { + + my $td = qq!'; + $col = $col eq $col1 ? $col2 : $col1; + } + + $OUT .= '
'. + $pkg->{'pkg'}. + '
". $cust_svc->{label}[0]. ':
!. + 'Setup '. $part_svc->{'svc'}. ' '. + '('. $part_svc->{'num_avail'}. ' available)'. + '

'; + +} %> + +
+
+powered by freeside + diff --git a/fs_selfservice/FS-SelfService/cgi/provision_svc_acct.html b/fs_selfservice/FS-SelfService/cgi/provision_svc_acct.html new file mode 100644 index 000000000..d18375c82 --- /dev/null +++ b/fs_selfservice/FS-SelfService/cgi/provision_svc_acct.html @@ -0,0 +1,66 @@ +MyAccount +MyAccount

+<%= $url = "$selfurl?session=$session_id;action="; ''; %> + +<%= include('myaccount_menu') %> +
+Setup <%= $svc %>

+ +<%= if ( $error ) { + $OUT .= qq!Error setting up $svc: $error!. + '

'; +} ''; %> + + + + + + + + + + + + + + + + + + +<%= + if ( $security_phrase ) { + $OUT .= < + + + +ENDOUT + } else { + $OUT .= ''; + } +%> +<%= + if ( @svc_acct_pop ) { + $OUT .= ''; + } else { + $OUT .= popselector(popnum=>$popnum, pops=>\@svc_acct_pop); + } +%> +
Username
Password
Re-enter Password
Security Phrase +
Access number'. + popselector( 'popnum' => $popnum, + 'pops' => \@svc_acct_pop, + 'init_popstate' => $init_popstate, + 'popac' => $popac, + 'acstate' => $acstate, + ). + '
+ + + +
+
+powered by freeside + + diff --git a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi index 6d6716ddc..d8e044a96 100644 --- a/fs_selfservice/FS-SelfService/cgi/selfservice.cgi +++ b/fs_selfservice/FS-SelfService/cgi/selfservice.cgi @@ -6,8 +6,11 @@ use subs qw(do_template); use CGI; use CGI::Carp qw(fatalsToBrowser); use Text::Template; -use FS::SelfService qw( login customer_info invoice payment_info - process_payment ); +use FS::SelfService qw( login customer_info invoice + payment_info process_payment + list_pkgs + part_svc_info provision_acct unprovision_svc + ); $template_dir = '.'; @@ -54,8 +57,9 @@ if ( $cgi->param('session') eq 'login' ) { $session_id = $cgi->param('session'); +#order|pw_list XXX ??? $cgi->param('action') =~ - /^(myaccount|view_invoice|make_payment|payment_results)$/ + /^(myaccount|view_invoice|make_payment|payment_results|logout|change_bill|change_ship|provision|provision_svc|process_svc_acct|delete_svc)$/ or die "unknown action ". $cgi->param('action'); my $action = $1; @@ -167,6 +171,63 @@ sub payment_results { } +sub logout { + FS::SelfService::logout( 'session_id' => $session_id ); +} + +sub provision { + list_pkgs( 'session_id' => $session_id ); +} + +sub provision_svc { + + my $result = part_svc_info( + 'session_id' => $session_id, + map { $_ => $cgi->param($_) } qw( pkgnum svcpart ), + ); + die $result->{'error'} if exists $result->{'error'} && $result->{'error'}; + + $result->{'svcdb'} =~ /^svc_(.*)$/ + #or return { 'error' => 'Unknown svcdb '. $result->{'svcdb'} }; + or die 'Unknown svcdb '. $result->{'svcdb'}; + $action .= "_$1"; + + $result; +} + +sub process_svc_acct { + + my $result = provision_acct ( + 'session_id' => $session_id, + map { $_ => $cgi->param($_) } qw( + pkgnum svcpart username _password _password2 sec_phrase popnum ) + ); + + if ( exists $result->{'error'} && $result->{'error'} ) { + warn "$result $result->{'error'}"; + $action = 'provision_svc_acct'; + return { + $cgi->Vars, + %{ part_svc_info( 'session_id' => $session_id, + map { $_ => $cgi->param($_) } qw( pkgnum svcpart ) + ) + }, + 'error' => $result->{'error'}, + }; + } else { + warn "$result $result->{'error'}"; + return $result; + } + +} + +sub delete_svc { + unprovision_svc( + 'session_id' => $session_id, + 'svcnum' => $cgi->param('svcnum'), + ); +} + #-- sub do_template { @@ -175,6 +236,7 @@ sub do_template { $cgi->delete_all(); $fill_in->{'selfurl'} = $cgi->self_url; + $fill_in->{'cgi'} = \$cgi; my $template = new Text::Template( TYPE => 'FILE', SOURCE => "$template_dir/$name.html", @@ -183,6 +245,28 @@ sub do_template { or die $Text::Template::ERROR; print $cgi->header( '-expires' => 'now' ), - $template->fill_in( HASH => $fill_in ); + $template->fill_in( PACKAGE => 'FS::SelfService::_selfservicecgi', + HASH => $fill_in + ); +} + +#*FS::SelfService::_selfservicecgi::include = \&Text::Template::fill_in_file; + +package FS::SelfService::_selfservicecgi; + +#use FS::SelfService qw(regionselector expselect popselector); +use FS::SelfService qw(popselector); + +sub include { + my $name = shift; + my $template = new Text::Template( TYPE => 'FILE', + SOURCE => "$main::template_dir/$name.html", + DELIMITERS => [ '<%=', '%>' ], + UNTAINT => 1, + ) + or die $Text::Template::ERROR; + + $template->fill_in(); + } diff --git a/fs_selfservice/FS-SelfService/cgi/view_invoice.html b/fs_selfservice/FS-SelfService/cgi/view_invoice.html index d2b012b5d..46f731890 100644 --- a/fs_selfservice/FS-SelfService/cgi/view_invoice.html +++ b/fs_selfservice/FS-SelfService/cgi/view_invoice.html @@ -1,12 +1,9 @@ MyAccount MyAccount

<%= $url = "$selfurl?session=$session_id;action="; ''; %> -
-MyAccount
- -
- -<-- back to MyAccount

+ +<%= include('myaccount_menu') %> +
 <%= $invoice_text %>
diff --git a/httemplate/view/cust_main.cgi b/httemplate/view/cust_main.cgi
index 4497713c9..4ed30b813 100755
--- a/httemplate/view/cust_main.cgi
+++ b/httemplate/view/cust_main.cgi
@@ -897,28 +897,14 @@ sub get_packages {
     $pkg{expire} = $cust_pkg->getfield('expire');
     $pkg{cancel} = $cust_pkg->getfield('cancel');
   
-    my %svcparts = ();
-
-    foreach my $pkg_svc (
-      qsearch('pkg_svc', { 'pkgpart' => $part_pkg->pkgpart })
-    ) {
-  
-      next if ($pkg_svc->quantity == 0);
-  
-      my $part_svc = qsearchs('part_svc', { 'svcpart' => $pkg_svc->svcpart });
-  
-      my $svcpart = {};
-      $svcpart->{svcpart} = $part_svc->svcpart;
-      $svcpart->{svc} = $part_svc->svc;
-      $svcpart->{svcdb} = $part_svc->svcdb;
-      $svcpart->{quantity} = $pkg_svc->quantity;
-      $svcpart->{count} = 0;
-  
-      $svcpart->{services} = [];
-
-      $svcparts{$svcpart->{svcpart}} = $svcpart;
-
-    }
+    my %svcparts = map {
+      $_->svcpart => {
+                       $_->part_svc->hash,
+                       'quantity' => $_->quantity,
+                       'count'    => $cust_pkg->num_cust_svc($_->svcpart),
+                       #'services' => [],
+                     };
+    } $part_pkg->pkg_svc;
 
     foreach my $cust_svc ( $cust_pkg->cust_svc ) {
       #warn "svcnum ". $cust_svc->svcnum. " / svcpart ". $cust_svc->svcpart. "\n";
@@ -930,18 +916,14 @@ sub get_packages {
       #false laziness with above, to catch extraneous services.  whole
       #damn thing should be OO...
       my $svcpart = ( $svcparts{$cust_svc->svcpart} ||= {
-        'svcpart'  => $cust_svc->svcpart,
-        'svc'      => $cust_svc->part_svc->svc,
-        'svcdb'    => $cust_svc->part_svc->svcdb,
+        $cust_svc->part_svc->hash,
         'quantity' => 0,
-        'count'    => 0,
-        'services' => [],
+        'count'    => $cust_pkg->num_cust_svc($cust_svc->svcpart),
+        #'services' => [],
       } );
 
       push @{$svcpart->{services}}, $svc;
 
-      $svcpart->{count}++;
-
     }
 
     $pkg{svcparts} = [ values %svcparts ];