From 5f8111de04a4a914c72a1642722476db4728339c Mon Sep 17 00:00:00 2001
From: Ivan Kohler <ivan@freeside.biz>
Date: Wed, 16 Jul 2014 06:17:05 -0700
Subject: [PATCH] REST API, RT#28181

---
 FS/FS/API.pm                         |  44 +--------------
 FS/FS/Mason.pm                       |   1 +
 FS/FS/Record.pm                      |  12 ++++
 FS/FS/UI/REST.pm                     |  38 +++++++++++++
 FS/FS/cust_bill.pm                   |   8 +++
 FS/FS/cust_main.pm                   |   1 +
 FS/FS/cust_main/API.pm               |  63 +++++++++++++++++++++
 FS/FS/cust_pay.pm                    |   7 +++
 FS/FS/cust_pkg.pm                    |   2 +-
 FS/FS/cust_pkg/API.pm                |  13 +++++
 FS/FS/cust_svc.pm                    |   7 +++
 FS/FS/part_export/internal_diddb.pm  |  12 +++-
 FS/FS/part_pkg.pm                    |   8 ++-
 FS/FS/part_pkg/API.pm                |  17 ++++++
 htetc/freeside-base2.4.conf          |   7 +++
 htetc/freeside-base2.conf            |   6 ++
 httemplate/REST/1.0/.cust_bill.swp   | Bin 0 -> 12288 bytes
 httemplate/REST/1.0/.cust_main.swp   | Bin 0 -> 12288 bytes
 httemplate/REST/1.0/.cust_pkg.swp    | Bin 0 -> 12288 bytes
 httemplate/REST/1.0/.part_pkg.swp    | Bin 0 -> 12288 bytes
 httemplate/REST/1.0/.phone_avail.swp | Bin 0 -> 12288 bytes
 httemplate/REST/1.0/.rate_detail.swp | Bin 0 -> 12288 bytes
 httemplate/REST/1.0/cust_bill        |  28 +++++++++
 httemplate/REST/1.0/cust_main        |  81 +++++++++++++++++++++++++++
 httemplate/REST/1.0/cust_pkg         |  39 +++++++++++++
 httemplate/REST/1.0/part_pkg         |  40 +++++++++++++
 httemplate/REST/1.0/phone_avail      |  25 +++++++++
 httemplate/REST/1.0/rate_detail      |  35 ++++++++++++
 28 files changed, 445 insertions(+), 49 deletions(-)
 create mode 100644 FS/FS/UI/REST.pm
 create mode 100644 FS/FS/cust_main/API.pm
 create mode 100644 FS/FS/cust_pkg/API.pm
 create mode 100644 FS/FS/part_pkg/API.pm
 create mode 100644 httemplate/REST/1.0/.cust_bill.swp
 create mode 100644 httemplate/REST/1.0/.cust_main.swp
 create mode 100644 httemplate/REST/1.0/.cust_pkg.swp
 create mode 100644 httemplate/REST/1.0/.part_pkg.swp
 create mode 100644 httemplate/REST/1.0/.phone_avail.swp
 create mode 100644 httemplate/REST/1.0/.rate_detail.swp
 create mode 100644 httemplate/REST/1.0/cust_bill
 create mode 100644 httemplate/REST/1.0/cust_main
 create mode 100644 httemplate/REST/1.0/cust_pkg
 create mode 100644 httemplate/REST/1.0/part_pkg
 create mode 100644 httemplate/REST/1.0/phone_avail
 create mode 100644 httemplate/REST/1.0/rate_detail

diff --git a/FS/FS/API.pm b/FS/FS/API.pm
index 2105409c5..a0f1dba2a 100644
--- a/FS/FS/API.pm
+++ b/FS/FS/API.pm
@@ -476,19 +476,6 @@ Returns general customer information. Takes a hash reference as parameter with t
 
 =cut
 
-#some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
-
-use vars qw( @cust_main_editable_fields @location_editable_fields );
-@cust_main_editable_fields = qw(
-  first last company daytime night fax mobile
-);
-#  locale
-#  payby payinfo payname paystart_month paystart_year payissue payip
-#  ss paytype paystate stateid stateid_state
-@location_editable_fields = qw(
-  address1 address2 city county state zip country
-);
-
 sub customer_info {
   my( $class, %opt ) = @_;
   my $conf = new FS::Conf;
@@ -498,39 +485,10 @@ sub customer_info {
   my $cust_main = qsearchs('cust_main', { 'custnum' => $opt{custnum} })
     or return { 'error' => 'Unknown custnum' };
 
-  my %return = (
-    'error'           => '',
-    'display_custnum' => $cust_main->display_custnum,
-    'name'            => $cust_main->first. ' '. $cust_main->get('last'),
-    'balance'         => $cust_main->balance,
-    'status'          => $cust_main->status,
-    'statuscolor'     => $cust_main->statuscolor,
-  );
-
-  $return{$_} = $cust_main->get($_)
-    foreach @cust_main_editable_fields;
-
-  for (@location_editable_fields) {
-    $return{$_} = $cust_main->bill_location->get($_)
-      if $cust_main->bill_locationnum;
-    $return{'ship_'.$_} = $cust_main->ship_location->get($_)
-      if $cust_main->ship_locationnum;
-  }
-
-  my @invoicing_list = $cust_main->invoicing_list;
-  $return{'invoicing_list'} =
-    join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
-  $return{'postal_invoicing'} =
-    0 < ( grep { $_ eq 'POST' } @invoicing_list );
-
-  #generally, the more useful data from the cust_main record the better.
-  # well, tell me what you want
-
-  return \%return;
+  $cust_main->API_getinfo;
 
 }
 
-
 =item location_info
 
 Returns location specific information for the customer. Takes a hash reference as parameter with the following keys: custnum,secret
diff --git a/FS/FS/Mason.pm b/FS/FS/Mason.pm
index 5476fd83e..b7aa35543 100644
--- a/FS/FS/Mason.pm
+++ b/FS/FS/Mason.pm
@@ -138,6 +138,7 @@ if ( -e $addl_handler_use_file ) {
   use FS::UI::Web qw(svc_url);
   use FS::UI::Web::small_custview qw(small_custview);
   use FS::UI::bytecount;
+  use FS::UI::REST qw( rest_auth rest_uri_remain encode_rest );
   use FS::Msgcat qw(gettext geterror);
   use FS::Misc qw( send_email send_fax ocr_image
                    states_hash counties cities state_label
diff --git a/FS/FS/Record.pm b/FS/FS/Record.pm
index a68442582..b226e177b 100644
--- a/FS/FS/Record.pm
+++ b/FS/FS/Record.pm
@@ -367,6 +367,9 @@ sub qsearch {
   my @bind_type = ();
   my $dbh = dbh;
   foreach my $stable ( @stable ) {
+
+    carp '->qsearch on cust_main called' if $stable eq 'cust_main' && $DEBUG;
+
     #stop altering the caller's hashref
     my $record      = { %{ shift(@record) || {} } };#and be liberal in receipt
     my $select      = shift @select;
@@ -994,6 +997,8 @@ sub AUTOLOAD {
     eval "use FS::$table";
     die $@ if $@;
 
+    carp '->cust_main called' if $table eq 'cust_main' && $DEBUG;
+
     my $pkey_value = $self->$column();
     my %search = ( $foreign_column => $pkey_value );
 
@@ -1122,6 +1127,13 @@ sub hashref {
   $self->{'Hash'};
 }
 
+#fallback
+sub API_getinfo {
+  my $self = shift;
+  +{ ( map { $_=>$self->$_ } $self->fields ),
+   };
+}
+
 =item modified
 
 Returns true if any of this object's values have been modified with set (or via
diff --git a/FS/FS/UI/REST.pm b/FS/FS/UI/REST.pm
new file mode 100644
index 000000000..b6503ba51
--- /dev/null
+++ b/FS/FS/UI/REST.pm
@@ -0,0 +1,38 @@
+package FS::UI::REST;
+use base qw( Exporter );
+
+use strict;
+use vars qw( @EXPORT_OK );
+use JSON::XS;
+use FS::UID qw( adminsuidsetup );
+use FS::Conf;
+
+@EXPORT_OK = qw( rest_auth rest_uri_remain encode_rest );
+
+sub rest_auth {
+  my $cgi = shift;
+  adminsuidsetup('fs_api');
+  my $conf = new FS::Conf;
+  die 'Incorrect shared secret'
+    unless $cgi->param('secret') eq $conf->config('api_shared_secret');
+}
+
+sub rest_uri_remain {
+  my($r, $m) = @_;
+
+  #wacky way to get this... surely there must be a better way
+
+  my $path = $m->request_comp->path;
+
+  $r->uri =~ /\Q$path\E\/?(.*)$/ or die "$path not in ". $r->uri;
+
+  $1;
+
+}
+
+sub encode_rest {
+  #XXX HTTP Accept header to send other formats besides JSON
+  encode_json(shift);
+}
+
+1;
diff --git a/FS/FS/cust_bill.pm b/FS/FS/cust_bill.pm
index 4fb4a7d47..7cee5d78a 100644
--- a/FS/FS/cust_bill.pm
+++ b/FS/FS/cust_bill.pm
@@ -3243,6 +3243,14 @@ sub re_X {
 
 }
 
+sub API_getinfo {
+  my $self = shift;
+  +{ ( map { $_=>$self->$_ } $self->fields ),
+     'owed' => $self->owed,
+     #XXX last payment applied date
+   };
+}
+
 =back
 
 =head1 CLASS METHODS
diff --git a/FS/FS/cust_main.pm b/FS/FS/cust_main.pm
index 9f382ac4f..f0a479997 100644
--- a/FS/FS/cust_main.pm
+++ b/FS/FS/cust_main.pm
@@ -8,6 +8,7 @@ use base qw( FS::cust_main::Packages
              FS::cust_main::Billing_ThirdParty
              FS::cust_main::Location
              FS::cust_main::Credit_Limit
+             FS::cust_main::API
              FS::otaker_Mixin FS::payinfo_Mixin FS::cust_main_Mixin
              FS::geocode_Mixin FS::Quotable_Mixin FS::Sales_Mixin
              FS::o2m_Common
diff --git a/FS/FS/cust_main/API.pm b/FS/FS/cust_main/API.pm
new file mode 100644
index 000000000..2637c7eba
--- /dev/null
+++ b/FS/FS/cust_main/API.pm
@@ -0,0 +1,63 @@
+package FS::cust_main::API;
+
+use strict;
+
+#some false laziness w/ClientAPI::Myaccount customer_info/customer_info_short
+
+use vars qw(
+  @cust_main_addl_fields @cust_main_editable_fields @location_editable_fields
+);
+@cust_main_addl_fields = qw(
+  agentnum salesnum refnum classnum usernum referral_custnum
+);
+@cust_main_editable_fields = qw(
+  first last company daytime night fax mobile
+);
+#  locale
+#  payby payinfo payname paystart_month paystart_year payissue payip
+#  ss paytype paystate stateid stateid_state
+@location_editable_fields = qw(
+  address1 address2 city county state zip country
+);
+
+sub API_getinfo {
+  my( $self, %opt ) = @_;
+
+  my %return = (
+    'error'           => '',
+    'display_custnum' => $self->display_custnum,
+    'name'            => $self->first. ' '. $self->get('last'),
+    'balance'         => $self->balance,
+    'status'          => $self->status,
+    'statuscolor'     => $self->statuscolor,
+  );
+
+  $return{$_} = $self->get($_)
+    foreach @cust_main_editable_fields;
+
+  unless ( $opt{'selfservice'} ) {
+    $return{$_} = $self->get($_)
+      foreach @cust_main_addl_fields;
+  }
+
+  for (@location_editable_fields) {
+    $return{$_} = $self->bill_location->get($_)
+      if $self->bill_locationnum;
+    $return{'ship_'.$_} = $self->ship_location->get($_)
+      if $self->ship_locationnum;
+  }
+
+  my @invoicing_list = $self->invoicing_list;
+  $return{'invoicing_list'} =
+    join(', ', grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list );
+  $return{'postal_invoicing'} =
+    0 < ( grep { $_ eq 'POST' } @invoicing_list );
+
+  #generally, the more useful data from the cust_main record the better.
+  # well, tell me what you want
+
+  return \%return;
+
+}
+
+1;
diff --git a/FS/FS/cust_pay.pm b/FS/FS/cust_pay.pm
index 775c353e4..1044e436e 100644
--- a/FS/FS/cust_pay.pm
+++ b/FS/FS/cust_pay.pm
@@ -892,6 +892,13 @@ sub unapplied_sql {
 
 }
 
+sub API_getinfo {
+ my $self = shift;
+ my @fields = grep { $_ ne 'payinfo' } $self->fields;
+ +{ ( map { $_=>$self->$_ } @fields ),
+  };
+}
+
 # _upgrade_data
 #
 # Used by FS::Upgrade to migrate to a new database.
diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm
index af1bd835e..915f2297b 100644
--- a/FS/FS/cust_pkg.pm
+++ b/FS/FS/cust_pkg.pm
@@ -1,5 +1,5 @@
 package FS::cust_pkg;
-use base qw( FS::cust_pkg::Search
+use base qw( FS::cust_pkg::Search FS::cust_pkg::API
              FS::otaker_Mixin FS::cust_main_Mixin FS::Sales_Mixin
              FS::contact_Mixin FS::location_Mixin
              FS::m2m_Common FS::option_Common
diff --git a/FS/FS/cust_pkg/API.pm b/FS/FS/cust_pkg/API.pm
new file mode 100644
index 000000000..f87eed345
--- /dev/null
+++ b/FS/FS/cust_pkg/API.pm
@@ -0,0 +1,13 @@
+package FS::cust_pkg::API;
+
+use strict;
+
+sub API_getinfo {
+  my $self = shift;
+
+  +{ ( map { $_=>$self->$_ } $self->fields ),
+   };
+
+}
+
+1;
diff --git a/FS/FS/cust_svc.pm b/FS/FS/cust_svc.pm
index 8fc929f29..df179f58a 100644
--- a/FS/FS/cust_svc.pm
+++ b/FS/FS/cust_svc.pm
@@ -904,6 +904,13 @@ sub tickets {
   (@tickets);
 }
 
+sub API_getinfo {
+  my $self = shift;
+  my $svc_x = $self->svc_x;
+ +{ ( map { $_=>$self->$_ } $self->fields ),
+    ( map { $svc_x=>$svc_x->$_ } $svc_x->fields ),
+  };
+}
 
 =back
 
diff --git a/FS/FS/part_export/internal_diddb.pm b/FS/FS/part_export/internal_diddb.pm
index bb9743328..8771ae883 100644
--- a/FS/FS/part_export/internal_diddb.pm
+++ b/FS/FS/part_export/internal_diddb.pm
@@ -81,7 +81,7 @@ sub get_dids {
                         })
            ];
 
-  } elsif ( $opt{'state'} ) { #return aracodes
+  } elsif ( $opt{'state'} ) { #return areacodes
 
     $hash{state} = $opt{state};
 
@@ -94,7 +94,15 @@ sub get_dids {
            ];
 
   } else { 
-    die "FS::part_export::internal_diddb::get_dids called without options\n";
+
+    #die "FS::part_export::internal_diddb::get_dids called without options\n";
+    return [ map { $_->npa. '-'. $_->nxx. '-'. $_->station }
+                 qsearch({ 'table'    => 'phone_avail',
+                           'hashref'  => \%hash,
+                           'order_by' => 'ORDER BY station',
+                        })
+           ];
+
   }
 
 }
diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm
index 2f0646740..2ad785939 100644
--- a/FS/FS/part_pkg.pm
+++ b/FS/FS/part_pkg.pm
@@ -1,5 +1,7 @@
 package FS::part_pkg;
-use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common );
+use base qw( FS::part_pkg::API
+             FS::m2m_Common FS::o2m_Common FS::option_Common
+           );
 
 use strict;
 use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack );
@@ -2017,8 +2019,8 @@ sub _pkgs_sql {
 #false laziness w/part_export & cdr
 my %info;
 foreach my $INC ( @INC ) {
-  warn "globbing $INC/FS/part_pkg/*.pm\n" if $DEBUG;
-  foreach my $file ( glob("$INC/FS/part_pkg/*.pm") ) {
+  warn "globbing $INC/FS/part_pkg/[a-z]*.pm\n" if $DEBUG;
+  foreach my $file ( glob("$INC/FS/part_pkg/[a-z]*.pm") ) {
     warn "attempting to load plan info from $file\n" if $DEBUG;
     $file =~ /\/(\w+)\.pm$/ or do {
       warn "unrecognized file in $INC/FS/part_pkg/: $file\n";
diff --git a/FS/FS/part_pkg/API.pm b/FS/FS/part_pkg/API.pm
new file mode 100644
index 000000000..3210aa0f2
--- /dev/null
+++ b/FS/FS/part_pkg/API.pm
@@ -0,0 +1,17 @@
+package FS::part_pkg::API;
+
+use strict;
+
+sub API_getinfo {
+  my $self = shift;
+  #my( $self, %opt ) = @_;
+
+  +{ ( map { $_=>$self->$_ } $self->fields ),
+     ( map { $_=>$self->option($_) }
+         qw(setup_fee recur_fee)
+     ),
+   };
+
+}
+
+1;
diff --git a/htetc/freeside-base2.4.conf b/htetc/freeside-base2.4.conf
index c4e93f839..36ce3a515 100644
--- a/htetc/freeside-base2.4.conf
+++ b/htetc/freeside-base2.4.conf
@@ -1,5 +1,6 @@
 PerlModule Apache2::compat
 
+PerlModule DBIx::Profile
 #PerlModule Apache::DBI
 
 PerlModule HTML::Mason
@@ -72,3 +73,9 @@ PerlSetVar FreesideHttpOnly 1
 <Directory %%%FREESIDE_DOCUMENT_ROOT%%%/rt/REST/1.0/NoAuth/>
     Satisfy any
 </Directory>
+
+<Directory %%%FREESIDE_DOCUMENT_ROOT%%%/REST/>
+    Satisfy any
+    SetHandler perl-script
+    PerlHandler HTML::Mason
+</Directory>
diff --git a/htetc/freeside-base2.conf b/htetc/freeside-base2.conf
index 1bbe90a59..e2d507a52 100644
--- a/htetc/freeside-base2.conf
+++ b/htetc/freeside-base2.conf
@@ -59,3 +59,9 @@ PerlSetVar FreesideHttpOnly 1
     Satisfy any
 </Directory>
 
+<Directory %%%FREESIDE_DOCUMENT_ROOT%%%/REST/1.0/>
+    Satisfy any
+    SetHandler perl-script
+    PerlHandler HTML::Mason
+</Directory>
+
diff --git a/httemplate/REST/1.0/.cust_bill.swp b/httemplate/REST/1.0/.cust_bill.swp
new file mode 100644
index 0000000000000000000000000000000000000000..44d4de7a94a29acefde64d29436755153ef9f719
GIT binary patch
literal 12288
zcmeI2%Wl&^6o#j)DIhMpE)J<v$D*lIio_xpAtV+c!J-8=fGjtjG^5&%>q{aO1(uX0
zh!>!|3ld1IS-=K~x1bCDN!%uBfvC%hGt#GtJ?ES`^EsO+-mE;nd!KHu+z=QSgjg(p
z+}K;cEf$u9P+d3Fw`Yx(FJ&6Jy;*TDbV0inOPMH7+U+!zLFBuswAb%!JhWF=Dt0qV
zQfE{7{;XQO&IZ`PNe#@4wd<=DowZoFN|!D^I%xrpX9H}24X^<=zy{a=8(;%$;1nB3
zqdD;s{h!Itu#nGlN9O#JCpN$a*Z><~18jf|umLu}2G{@_U;}L66dG_nA@<J-u{)3E
z@&Et9@Bg3Yh4>D>fzRL*cnx;JJz#<*a0Ofj`{#uC1-^nk@E-I)7d!w3@B@2%0Uy9S
z@D{uQdhb_&2OD4mY=8~00XDz}*Z><~18m@bFi^9LDpYAhh=HJq@vJ&^saK4{m!!*$
z!;en1y>%>-Xu6or*5g{A0ym;Q8BV=1E~jgh8-_yzy46zB_YxZD?TuKbSsYTGp5%GK
z^NB8W8rBR)$71Cmw@#xXC3C7v)2iZ-B>hCSsDwJ5AaFyE<PIGPB#ZiDYX849{->t@
z-N>+s<NKSVE%ounXT~Ey6P=hmC8>~w+hL~{k_x+F7LYNT3so`Y1U_~W>Bh}=GI4x!
z8P3g7h6(d|jR9E#>2NW+u{@Syp->X}DlM5dvSW$Y<Fi;fu?$=lmW&v&fkj&BaLvuq
hcFAaNDSfAwf2C?gl3}ysNu(tyl7^4dqGIEz_zh{@?2iBd

literal 0
HcmV?d00001

diff --git a/httemplate/REST/1.0/.cust_main.swp b/httemplate/REST/1.0/.cust_main.swp
new file mode 100644
index 0000000000000000000000000000000000000000..d785784d38797fac52d412ec4b0a607cbf00e9ae
GIT binary patch
literal 12288
zcmeI2O>Y}T7{{j^cyB@Np(mt=bj|JxCyrAj4vj5JYapU&OPoR_+FBEPY_GDrc6Qed
zAur_s0xlJwpnL%&Bsg-U6sSa7P9RZ%gt*aifD_OI|Jj{gn>3MQaVnZse%{%6eda&&
zJYy@nF!}1tEIm8%48!#}V|(AcQhlQOJTsnR%sJ0}nLX@kx<W)<zC5hnhU~D=41@@s
zhA6b7NO)bBN1`xSsm>RQ6O)B{FN|s)cXl-Hu<>-cRzNE-sK9P^aH=>da}JC>N&EJm
z8Z6Xlt$<cQE1(t73TOqi0$KsBfL7ptu0Yh?#V%lu_o;clqVBsk-PNPMX$7<bS^=$q
zRzNGD70?Q31+)TM0j+>mKr3($D!?0zHSTBZ@&hm)|Nl>a|NrtBV>iJM;41hUyaV0_
zEzks2FbB$@1peH^*bQ(Ud<s4R?}PV11snzAAP;`q&Dd>l8GH`j0vEt(upj*XC}Tf?
zAHnzFJ8%Vj0WN}BkOO<boktk^3;YZ|26b>8><3pLX6zDp7i@rOaPuL?z62kDBVZr6
z^B`k?fZN~}xCTA~7l8)^Xn-P^1bOf@xC7n(2H%1k;2QV_NIky-AA(EZ95@ROf+=td
zn*Rzu1NzbmXa%$aT7gj%n4T`=9N&q`jIHC3gc~|dvd9R=%btkZod$^|GEbhGH_4_|
zIxunruhVdvE1UZsp_>t^CMIqkac3QjD#Yed?$+!?H5IKM2)v+2zQEgkL=c*A8G(p;
zflnoRojl$pqc&bXd~&AN5|QIKJ9&C8dykLK9!lD*+lS(|M5cHn3V1DCa!o3g$;ckF
z3T+;?1JN`w6e`e}oXp6l%_F|x3RxDHn%PihQo~yr9KJ+Lq2NKiZLKOhR5xSkXmLmV
z4_lS@a|Tj}j~}Im2qT&~HuG|Y44K}({%bE)<|>rgHz8Ag&x34PUd{2(cbvLN%d*ar
zww2Q})%hwmb+vM=a%7&0^upYU*}I#oW_>9!#+b^49<iOsYAm$J%S*jZBrG#$PEc6q
zE)OiD-g3svT^?}H>dTp$R+E#gg}sF^3LL*>+V%vYwOY;rzZ<O=x#5+C|7|Hs_wq9A
zd<CGpl01oW!__wJOk=qRGB?9UFiq_KY_CyrWe%#I)7!Ql0T&fPz}=eK*eq=^a9>)s
zh_{3vZ~TzEB9tD0mhx72c^E(ULJ=S%?f|yN{th#(IJ&GNzQ{PRg;qw*wv(U~??WNO
zyz9Ux<DOKs8n8x2qru01dQq%|l<O|G;0Dpwm1Lw-!6dXyS+9orW*&o@s-amb!`5{>
zja9<}Wnw3hoU`fz)0P9tNy8@-d*tkkv`$7AH^$3x66I-l?Vo@3j^}YdIsGy#<E^0P
zKFq3rjbo^@G>zl2v1ulJueiMdr41^avtmOnSh~ieh}YX5HpAM2<GO2IzGB-(VQbR{
z=Q?n<sL_-U49qk><WoI60W};!V?BS-?=1Uqz?2&<?i2|Xaim*hl0x|Y>Snpg^2$dv
zlgq1;SPff_rVLS$UB)zI%6|Jtq*QLJZzeBRl0z0hFjsH{tB&(XscG3TDeSsVWSIrn
z5y&{)3!GXY<Qwvq5umPT=V^-Ix-2f{y{H|>bD1-(z8_8JNci<mL!f0Cp<R4>+mtJx
GVgCRGY3p79

literal 0
HcmV?d00001

diff --git a/httemplate/REST/1.0/.cust_pkg.swp b/httemplate/REST/1.0/.cust_pkg.swp
new file mode 100644
index 0000000000000000000000000000000000000000..451a98554b9bfb2e7d243f943a2e1aa8e7b0788d
GIT binary patch
literal 12288
zcmeI2O=}ZD7{{j`^kA!^M?s$^8#jTr+g8Ct+w}#C^dOcd^`a8i%_JF`-A#9ATdGvO
z=vhC3SBriD{Q!b~fcBtA!EYh{XLmzW5kilJXULD)%*!*+{AMp9^K|y%%6+;yb3>q9
z5aRN)ul2pPyCS(Jgxc~#(>$uQds6DYw>@gy3lq?3$5JM$C7rI;GU)d_EuFRHdc&EU
znRWa$(aruwXH+QeX9H~D*ajxV_4&D3Gp$^jq)Qjqj~#(0umLu}2G{@_U;}J`4X^<=
zaB>ak{+M`)^iLIAu#0-^pSt+U6&qj!Y=8~00XDz}*Z><~18jf|umLu30u6XAA%2ew
z@qPly<NyD`@Bd%th4=}6fbZZNcmrO8F6e*;sDs<!7Wi{chy$<>zJSl*9e4{mpbe_P
z29tomRd5B2gR|faI1LVv&p!ABK7u{)0hl~r0WNHS4X^<=zy{a=8(;%$fDN#LlV)Ji
zsi;tCSBPDFNcIxdrYc$Sv>Qm>9nl@NNN$q7y56v<M$f4nL@m|c`Cp{L&<FLwSU;Ay
zdrhIl#cd)iPgKq_kS0=3CECa&l&sG@Sh3V;9MTd!qQL8u)tq+ktgbXWQme2XO_4RY
zz&-6|Psx@~PZo+Ohr9EmAn?LG`N6ns2l>e1g@z&ytyF}}T@6P&5+#bMr6iToa3hSi
zL&C-KFb&Ac>(YWSdkS_Av&P0Y6Y0f%H>uiLVu7bZdkRAAe3HH77Sm~$Y9`%0haxl%
z!L*r}hpLQ#Bd1!N#l7YHD1-=haO8Oi<FK$(Lpn*nr*zeJkV7nSH$IJ3GnOW+sug2w
rP@AH8s$qq!+e>w~YWW>yS{92}Yq3Hy^rM!<%tT{ec8E2qxR1qO|IY8X

literal 0
HcmV?d00001

diff --git a/httemplate/REST/1.0/.part_pkg.swp b/httemplate/REST/1.0/.part_pkg.swp
new file mode 100644
index 0000000000000000000000000000000000000000..547a79eee70e909867fa3ae423c06424560a50df
GIT binary patch
literal 12288
zcmeI2O-~a+7{>>%iZ6IXlSf$G-GowniAX2~Ogvy>Vg!{$Ofz(+bi=;1yHg1g^r(IT
zJ!<sm(X$>+)GwfsqZhx0|Jk;**bqcdB+n*4c6VN$dFD4$0_m)b-<`in6JwVJu9HGM
z@;)uhOkNXb&kNx{l#wak@2Uh!>6%>Muiax4FkeZON_|h|t6HnD7D%n~x34cO<}Zwm
z=W8<2rP|u+exbOY4X}ZO7-;B9?rOidbn(Kt={7ucj!qBUIY<O<%Ldp08(;%$fDNz#
zHoykhz#%uFYklGg=5VAn52tnS`{Ukv<r^Dd18jf|umLu}2G{@_U;}J`4X^<=a0m@Z
zPl)<4A#(jl9^e1B|NsAaLWphf1AGCW!87m_RDc4DU;)g5Ss=jo<3fA`AHfIk61)Hr
zxB&>90fS%woC4bz`#pFI-T)0!Pykb465y*#F#)aslj|V(io9QgSKv7?^SB2r@aw1$
z?*LzHfDNz#Hoykh02^QfY~YX@DC9@{$k#=a{ucg{?z$dK?RqsevpYv=o9+Ii_dj&q
zyUSiRS$R)op{K-3lx`#uP1Ah}WsR)%JaY$VTAaHzUs_e#k1FwKM-{;%M1~t#QE$B7
zl0B38s}ZuJ&X`F(RWd189lJdjdz6+(sxqy*SwhVf%zmPv>#g}%Uw6p!Q@Ii-kII2e
z)2I=Wi-wp<76xNDnXOH-N;F-hv@8ReI97Sp&lNGM3>~ZFx`chTMJh-YZ5k6>0N3!+
z(h?=g^AlCpq+^m)Zlt;tN<X4z?laAnp0CI@t)p1ehSKEYgtrP+q?Koz)U)V*1?gDj
zI1FXvk*bqz!o{IVQk&f7&_tb;?yPaQ)=W)rsICZc0xyb&8qr!5uSbM~<;F1fO!HyL
zWCTY$@jGjg?plmZvRhs3>0-M#=HlXzd^VSM<Scm;v9mGEo-#YC;;X<*F+%g9E52Fe
za05nKQARs%T(nPXfv+7qk4=#%SZ^eLDN)9~V<l)Cx@00`b+VzW*{Yh7LhJWMVT4pv
Tjy;8LsYa*l6XeoJaasHZ*g<8?

literal 0
HcmV?d00001

diff --git a/httemplate/REST/1.0/.phone_avail.swp b/httemplate/REST/1.0/.phone_avail.swp
new file mode 100644
index 0000000000000000000000000000000000000000..a65bb2748f3a7bfff6877043883065d4bb49031b
GIT binary patch
literal 12288
zcmeI2&2G~`5XUz?Q>fs8dO_1PA+|)mno1m!SO}Fv4_qi82SC==-X<&Twd>s^t*XLv
z01v@|6E9F6fpS8;1s5b{9h)FHM7>dIR{G11v-6*s-|ofo?suN<K7_liZH8lwvGcXF
zy>IXDvs<?rlT)tj<7ML@62hc>wrt*OYuG->gwQe+?XfW;P9tta`_Y5F$L(&b(@w{U
z5<Z`D87*_8|A_z*xEO&|*4yfKY|LhL18!V@a<OolO$3Ml5g-CYfCvx)B0vO)z@;W&
z(h7Tn9jp{xtQXhHU)SO%U5Ee?AOb{y2oM1xKm>>Y5g-CYfCvzQOGto+jD5VqSZ@`J
z$M65;`~UZAjD1C&qdud~Q14K0Q5uz@dZ-mt1@+}BW1mnd>KSSsbrbayeSe@npg3xX
z8lY^xEfftRKm>>Y5g-CYfCvx)B0vO)z<*3&(5_1*jmH=)8*YF39P4p7b0EWm(1B1!
zWS|Y+1J48xl2Cv+3dUTG1h_0dhFX3`T1{dNL#T<PG|5cU8wulwGSm*lJcSde`7rcA
z2R!1LQwv72>7_j5u~YM17f$oCa<{p`!b(5yyzDbOA(R5DDuJEd9Z0ieDnqG8Sd2{w
z+!!8=MF=JVJq^&?K&`l`VmydQ%L8y=1Mba3G`Mh5+QvyHz+hK^=e=!)=0!8)=0$&H
z1{XZ&=4p^q`Ovmz%k#0i9D?!Z1zBEw#_~2N>Xk}n>fcsiq9UR7!lFN4gTfHa54GS~
zFxF0^%+k1n3(zQFG=^Tu1x^7sL>^xv!`l8>o8K{Z+NNy8u+O!`HH?mdgO+$&9(PA9
tbud3DZ#O{;<MDM|XyfyV8Ru(l!w1DC9n?W6yN!H1$64$i7wX;%_6ui>4-NnT

literal 0
HcmV?d00001

diff --git a/httemplate/REST/1.0/.rate_detail.swp b/httemplate/REST/1.0/.rate_detail.swp
new file mode 100644
index 0000000000000000000000000000000000000000..8d46a280f00f95becd547d1b0486b5d0f3e090a5
GIT binary patch
literal 12288
zcmeI2&1w@-6vt0p7HdT}?mV{B&V;s^Mn#1-Y5j<_U{#uaxCjonnVV)LNz<7dwS?5C
zP`c1Z@F8^JUQ|#J7lP>0NATR-nK~(AyDgjxe=g_T^L2kSn~>R<URzwktJ%vy>omZR
z!FQ!|FK@v4ae&~lXlnOit6G;*wZxOd;&ydGw;D<r1-^7^O36m6E|heai=`Demz{P)
zvOHfZ5!4T>#^+3c2^^U~TUDp74Xs|fn48u)qazpa?3vXgr*j|^U;<2l2`~XBzyz28
z6JP?zpMYu&z%#nWiTEm`@jh^1k4@f~025#WOn?b60Vco%m;e)C0!)AjFo9!8K==Sx
zP6B)*>Hq(e-~V5S0X~vGklvEskY1B^NP={qG)nq(3g8uKkMx4{ob-q^M=FpeNfv34
z^koR(GwD5PkK~coNo%A9(l}|D^qXe+Mfw@f^<B@#iwQ6RCcp%k025#WOn?b60VeQY
z0<-Q|&<s=o0BLnIGrb*AK2@RcqU}273ux_Ilku@#6HzUcRa+l-y8CV=HtN!*O60av
zpDNorqKn&+6k(<2bWA(!ChZ(-h!%Fx@_PMxnW5YawwhA~@^x|ESY9=dbw9#P((zx3
z=IiJr(O3r(C*lbjJ7}evVLmpK626R-XIh(4_Y+KnPVn?eZ+xKB`P8j3560EINisqn
z%_STI$W*t9yw2{Q_CTmP)S=L&o+Al8;@!Wl-@S#Et#(s|JC!Y8(v-H9vTf)OkFE8E
z;&KsF^Ji1nZ?+p$CdDdi1}A^7nn^A`SS+oS(7{siPVwdn=J59Ny`^*-1^<1WsNx7Y
zG}P318ftC^f-2RqDx1Mnp(R4maO_l!ox%Jrx(~|}b57Zx$XG6Bu%i+7Rkv+o73zm5
h+N$R0YMS0`{L;^kp=@eG^o%3rV7c$<WpJ!ehCf56BN_kz

literal 0
HcmV?d00001

diff --git a/httemplate/REST/1.0/cust_bill b/httemplate/REST/1.0/cust_bill
new file mode 100644
index 000000000..926cf3bee
--- /dev/null
+++ b/httemplate/REST/1.0/cust_bill
@@ -0,0 +1,28 @@
+<% encode_rest($return) %>\
+<%init>
+
+rest_auth($cgi);
+
+my( $invnum, $command ) = split('/', rest_uri_remain($r, $m) );
+
+my $cust_bill = qsearchs('cust_bill', { 'invnum'=>$invnum } )
+  or die "unknown invnum $invnum";
+
+my $return = [];
+
+if ( $command eq '' ) {
+
+  my @fields = fields('cust_bill');
+  $return = +{ map { $_=>$cust_bill->$_ } @fields };
+
+} elsif ( $command eq 'cust_bill_pkg' ) {
+
+  my @fields = fields('cust_bill_pkg');
+  $return = [ map { my $cust_bill_pkg = $_;
+                    +{ map { $_=>$cust_bill_pkg->$_ } @fields };
+                  }
+                $cust_bill->cust_bill_pkg
+            ];
+}
+
+</%init>
diff --git a/httemplate/REST/1.0/cust_main b/httemplate/REST/1.0/cust_main
new file mode 100644
index 000000000..89c558cc2
--- /dev/null
+++ b/httemplate/REST/1.0/cust_main
@@ -0,0 +1,81 @@
+<% encode_rest($return) %>\
+<%init>
+
+rest_auth($cgi);
+
+my( $custnum, $command ) = split('/', rest_uri_remain($r, $m), 2 );
+
+if ( $r->method eq 'GET' ) {
+
+  my $return = [];
+
+  if ( $custnum ) {
+
+    my $cust_main = qsearchs('cust_main', { 'custnum'=>$custnum } )
+      or die "unknown custnum $custnum";
+
+    if ( $command eq '' ) {
+
+      $return = $cust_main->API_getinfo;
+
+    } elsif ( $command =~ /^(cust_(pkg|attachment|bill|pay))$/ ) {
+
+      my $method = $1;
+
+      $return = [ map $_->API_getinfo, $cust_main->$method ];
+
+    } elsif ( $command eq 'part_pkg' ) {
+
+      my %pkgpart = map { $_->pkgpart => 1 } $cust_main->cust_pkg;
+
+      $return = [ map $_->API_getinfo,
+                    map qsearchs('part_pkg', { 'pkgpart'=>$_ }),
+                      keys %pkgpart;
+                ];
+
+    }
+
+  } else { #list
+
+    my %hash = ( map { $_ => scalar($cgi->param($_)) }
+                   qw( agentnum salesnum refnum classnum usernum
+                       referral_custnum
+                     )
+               );
+ 
+    my $extra_sql = '';
+    if ( $cgi->param('cust_main_invoice_dest') ) {
+      my $dest = dbh->quote(scalar($cgi->param('cust_main_invoice_dest')));
+      $extra_sql = "
+        WHERE EXISTS ( SELECT 1 FROM cust_main_invoice
+                         WHERE cust_main.custnum = cust_main_invoice.custnum
+                           AND dest = $dest
+                     )
+      ";
+    } elsif ( $cgi->param('cust_main_invoice_dest_substring') ) {
+      my $dest = dbh->quote('%'. scalar($cgi->param('cust_main_invoice_dest_substring')). '%');
+      $extra_sql = "
+        WHERE EXISTS ( SELECT 1 FROM cust_main_invoice
+                         WHERE cust_main.custnum = cust_main_invoice.custnum
+                           AND dest ILIKE $dest
+                     )
+      ";
+    }
+
+    my @cust_main = qsearch({
+      'table'     => 'cust_main',
+      'hashref'   =>  \%hash,
+      'extra_sql' => $extra_sql;
+    });
+
+    $return = [ map $_->API_getinfo, @cust_main ];
+
+  }
+
+} elsif ( $r->method eq 'POST' ) { #create new
+
+} elsif ( $r->method eq 'PUT' ) { #modify
+
+}
+
+</%init>
diff --git a/httemplate/REST/1.0/cust_pkg b/httemplate/REST/1.0/cust_pkg
new file mode 100644
index 000000000..3c58bcf31
--- /dev/null
+++ b/httemplate/REST/1.0/cust_pkg
@@ -0,0 +1,39 @@
+<% encode_rest($return) %>\
+<%init>
+
+rest_auth($cgi);
+
+my( $pkgnum, $command ) = split('/', rest_uri_remain($r, $m), 2 );
+
+if ( $r->method eq 'GET' ) {
+
+  my $return = [];
+
+  if ( $pkgnum ) {
+
+    my $cust_pkg = qsearchs('cust_main', { 'pkgnum'=>$pkgnum } )
+      or die "unknown pkgnum $pkgnum";
+
+    if ( $command eq '' ) {
+
+      $return = $cust_pkg->API_getinfo;
+
+    } elsif ( $command eq 'cust_svc' ) {
+
+      $return = [ map $_->API_getinfo, $cust_pkg->cust_svc ];
+
+    }
+
+
+
+  #} else { #list
+
+  }
+
+} elsif ( $r->method eq 'POST' ) { #create new
+
+} elsif ( $r->method eq 'PUT' ) { #modify
+
+}
+
+</%init>
diff --git a/httemplate/REST/1.0/part_pkg b/httemplate/REST/1.0/part_pkg
new file mode 100644
index 000000000..c81b7b89b
--- /dev/null
+++ b/httemplate/REST/1.0/part_pkg
@@ -0,0 +1,40 @@
+<% encode_rest($return) %>\
+<%init>
+
+rest_auth($cgi);
+
+my( $pkgpart, $command ) = split('/', rest_uri_remain($r, $m) );
+
+my @fields = fields('part_pkg');
+
+my $return = [];
+
+if ( $pkgpart ) {
+
+  my $part_pkg = qsearchs('part_pkg', { 'pkgpart'=>$pkgpart } )
+    or die "unknown pkgpart $pkgpart";
+
+  if ( $command eq '' ) {
+
+    $return = $part_pkg->API_getinfo;
+
+  } elsif ( $command eq 'customers' ) {
+     die 'XXX not yet implemented';
+     #XXX redirect to a cust_main search?
+  }
+
+} else {
+
+  my %hash = ( map { $_ => scalar($cgi->param($_)) }
+                 qw( disabled classnum )
+             );
+
+  my @part_pkg = qsearch('part_pkg', \%hash);
+
+  $return = [ map $part_pkg->API_getinfo, @part_pkg ];
+
+}
+
+
+
+</%init>
diff --git a/httemplate/REST/1.0/phone_avail b/httemplate/REST/1.0/phone_avail
new file mode 100644
index 000000000..ef9d3e7f0
--- /dev/null
+++ b/httemplate/REST/1.0/phone_avail
@@ -0,0 +1,25 @@
+<% encode_rest($phonenums) %>\
+<%init>
+
+rest_auth($cgi);
+
+#i'm basically a simpler misc/phonenums.cgi
+
+my $svcpart = $cgi->param('svcpart');
+
+my $part_svc = qsearchs('part_svc', { 'svcpart'=>$svcpart } );
+die "unknown svcpart $svcpart" unless $part_svc;
+
+my @exports = $part_svc->part_export_did;
+if ( scalar(@exports) > 1 ) {
+  die "more than one DID-providing export attached to svcpart $svcpart";
+} elsif ( ! @exports ) {
+  die "no DID providing export attached to svcpart $svcpart";
+}
+my $export = $exports[0];
+    
+my $phonenums = $export->get_dids( map { $_ => scalar($cgi->param($_)) }
+                                     qw( ratecenter state areacode exchange )
+                                 );
+
+</%init>
diff --git a/httemplate/REST/1.0/rate_detail b/httemplate/REST/1.0/rate_detail
new file mode 100644
index 000000000..54e55de6c
--- /dev/null
+++ b/httemplate/REST/1.0/rate_detail
@@ -0,0 +1,35 @@
+<% encode_rest( \@rate_detail ) %>\
+<%init>
+
+rest_auth($cgi);
+
+my $extra_sql = '';
+if ( $cgi->param('countrycode') =~ /^\+?(\d+)$/ ) {
+  my $countrycode = $1;
+  $extra_sql = "
+    WHERE EXISTS ( SELECT 1 rate_region
+                     WHERE rate_detail.dest_regionnum = rate_region.regionnum
+                       AND countrycode = '$countrycode'
+  ";
+}
+
+my @detail_fields = fields('rate_detail');
+my @region_fields = fields('rate_region');
+
+my @rate_detail =
+  map {
+    my $rate_detail = $_;
+    my $rate_region = $rate_detail->dest_region;
+
+    +{
+      ( map { $_ => $rate_detail->$_ } @detail_fields ),
+      ( map { $_ => $rate_region->$_ } @region_fields ),
+     };
+
+  } qsearch({
+      'table'   => 'rate_detail',
+      'hashref' => {},
+      extra_sql => $extra_sql,
+    });
+
+</%init>
-- 
2.20.1