},
{
- 'key' => 'batch-manual_approval',
- 'section' => 'billing',
- 'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status. This is not advised, but is needed for payment processors that provide a report of rejected rather than approved payments.',
- 'type' => 'checkbox',
- },
-
- {
'key' => 'batchconfig-eft_canada',
'section' => 'billing',
'description' => 'Configuration for EFT Canada batching, four lines: 1. SFTP username, 2. SFTP password, 3. Transaction code, 4. Number of days to delay process date.',
},
{
+ 'key' => 'batchconfig-nacha-destination',
+ 'section' => 'billing',
+ 'description' => 'Configuration for NACHA batching, Destination (9 digit transit routing number).',
+ 'type' => 'text',
+ },
+
+ {
+ 'key' => 'batchconfig-nacha-destination_name',
+ 'section' => 'billing',
+ 'description' => 'Configuration for NACHA batching, Destination (Bank Name, up to 23 characters).',
+ 'type' => 'text',
+ },
+
+ {
+ 'key' => 'batchconfig-nacha-origin',
+ 'section' => 'billing',
+ 'description' => 'Configuration for NACHA batching, Origin (your 10-digit company number, IRS tax ID recommended).',
+ 'type' => 'text',
+ },
+
+ {
+ 'key' => 'batch-manual_approval',
+ 'section' => 'billing',
+ 'description' => 'Allow manual batch closure, which will approve all payments that do not yet have a status. This is not advised unless needed for specific payment processors that provide a report of rejected rather than approved payments.',
+ 'type' => 'checkbox',
+ },
+
+ {
'key' => 'batch-spoolagent',
'section' => 'billing',
'description' => 'Store payment batches per-agent.',
--- /dev/null
+package FS::part_export::huawei_hlr;
+
+use vars qw(@ISA %info $DEBUG $CACHE);
+use Tie::IxHash;
+use FS::Record qw(qsearch qsearchs dbh);
+use FS::part_export;
+use FS::svc_phone;
+use IO::Socket::INET;
+use Data::Dumper;
+
+use strict;
+
+$DEBUG = 0;
+@ISA = qw(FS::part_export);
+
+tie my %options, 'Tie::IxHash',
+ 'opname' => { label=>'Operator login' },
+ 'pwd' => { label=>'Operator password' },
+ 'tplid' => { label=>'Template number' },
+ 'hlrsn' => { label=>'HLR serial number' },
+ 'debug' => { label=>'Enable debugging', type=>'checkbox' },
+;
+
+%info = (
+ 'svc' => 'svc_phone',
+ 'desc' => 'Provision mobile phone service to Huawei HLR9820',
+ 'options' => \%options,
+ 'notes' => <<'END'
+Connects to a Huawei Subscriber Management Unit via TCP and configures mobile
+phone services according to a template. The <i>sim_imsi</i> field must be
+set on the service, and the template must exist.
+END
+);
+
+sub _export_insert {
+ my( $self, $svc_phone ) = (shift, shift);
+ # svc_phone::check should ensure phonenum and sim_imsi are numeric
+ my @command = (
+ 'ADD TPLSUB',
+ IMSI => '"'.$svc_phone->sim_imsi.'"',
+ ISDN => '"'.$svc_phone->phonenum.'"',
+ TPLID => $self->option('tplid'),
+ );
+ unshift @command, 'HLRSN', $self->option('hlrsn')
+ if $self->option('hlrsn');
+ my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
+ ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub _export_replace {
+ my( $self, $new, $old ) = @_;
+ my $depend_jobnum;
+ if ( $new->sim_imsi ne $old->sim_imsi ) {
+ my @command = (
+ 'MOD IMSI',
+ ISDN => '"'.$old->phonenum.'"',
+ IMSI => '"'.$old->sim_imsi.'"',
+ NEWIMSI => '"'.$new->sim_imsi.'"',
+ );
+ my $err_or_queue = $self->queue_command($new->svcnum, @command);
+ return $err_or_queue unless ref $err_or_queue;
+ $depend_jobnum = $err_or_queue->jobnum;
+ }
+ if ( $new->phonenum ne $old->phonenum ) {
+ my @command = (
+ 'MOD ISDN',
+ ISDN => '"'.$old->phonenum.'"',
+ NEWISDN => '"'.$new->phonenum.'"',
+ );
+ my $err_or_queue = $self->queue_command($new->svcnum, @command);
+ return $err_or_queue unless ref $err_or_queue;
+ if ( $depend_jobnum ) {
+ my $error = $err_or_queue->depend_insert($depend_jobnum);
+ return $error if $error;
+ }
+ }
+ # no other svc_phone changes need to be exported
+ '';
+}
+
+sub _export_suspend {
+ my( $self, $svc_phone ) = (shift, shift);
+ $self->_export_lock($svc_phone, 'TRUE');
+}
+
+sub _export_unsuspend {
+ my( $self, $svc_phone ) = (shift, shift);
+ $self->_export_lock($svc_phone, 'FALSE');
+}
+
+sub _export_lock {
+ my ($self, $svc_phone, $lockstate) = @_;
+ # XXX I'm not sure this actually suspends. Need to test it.
+ my @command = (
+ 'MOD LCK',
+ IMSI => '"'.$svc_phone->sim_imsi.'"',
+ ISDN => '"'.$svc_phone->phonenum.'"',
+ IC => $lockstate,
+ OC => $lockstate,
+ GPRSLOCK=> $lockstate,
+ );
+ my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
+ ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub _export_delete {
+ my( $self, $svc_phone ) = (shift, shift);
+ my @command = (
+ 'RMV SUB',
+ IMSI => '"'.$svc_phone->sim_imsi.'"',
+ ISDN => '"'.$svc_phone->phonenum.'"',
+ );
+ my $err_or_queue = $self->queue_command($svc_phone->svcnum, @command);
+ ref($err_or_queue) ? '' : $err_or_queue;
+}
+
+sub queue_command {
+ my ($self, $svcnum, @command) = @_;
+ my $queue = FS::queue->new({
+ svcnum => $svcnum,
+ job => 'FS::part_export::huawei_hlr::run_command',
+ });
+ $queue->insert($self->exportnum, @command) || $queue;
+}
+
+sub run_command {
+ my ($exportnum, @command) = @_;
+ my $self = FS::part_export->by_key($exportnum);
+ my $socket = $self->login;
+ my $result = $self->command($socket, @command);
+ $self->logout($socket);
+ $socket->close;
+ die $result->{error} if $result->{error};
+ '';
+}
+
+sub login {
+ my $self = shift;
+ local $DEBUG = $self->option('debug') || 0;
+ # Send a command to the SMU.
+ # The caller is responsible for quoting string parameters.
+ my %socket_param = (
+ PeerAddr => $self->machine,
+ PeerPort => 7777,
+ Proto => 'tcp',
+ Timeout => ($self->option('timeout') || 30),
+ );
+ warn "Connecting to ".$self->machine."...\n" if $DEBUG;
+ warn Dumper(\%socket_param) if $DEBUG;
+ my $socket = IO::Socket::INET->new(%socket_param)
+ or die "Failed to connect: $!\n";
+
+ warn 'Logging in as "'.$self->option('opname').".\"\n" if $DEBUG;
+ my @login_param = (
+ OPNAME => '"'.$self->option('opname').'"',
+ PWD => '"'.$self->option('pwd').'"',
+ );
+ if ($self->option('HLRSN')) {
+ unshift @login_param, 'HLRSN', $self->option('HLRSN');
+ }
+ my $login_result = $self->command($socket, 'LGI', @login_param);
+ die $login_result->{error} if $login_result->{error};
+ return $socket;
+}
+
+sub logout {
+ warn "Logging out.\n" if $DEBUG;
+ my $self = shift;
+ my ($socket) = @_;
+ $self->command($socket, 'LGO');
+ $socket->close;
+}
+
+sub command {
+ my $self = shift;
+ my ($socket, $command, @param) = @_;
+ my $string = $command . ':';
+ while (@param) {
+ $string .= shift(@param) . '=' . shift(@param);
+ $string .= ',' if @param;
+ }
+ $string .= "\n";
+ my @result;
+ eval { # timeout
+ local $SIG{ALRM} = sub { die "timeout\n" };
+ alarm ($self->option('timeout') || 30);
+ warn "Sending to server:\n$string\n\n" if $DEBUG;
+ $socket->print($string);
+ warn "Received:\n";
+ my $line;
+ do {
+ $line = $socket->getline();
+ warn $line if $DEBUG;
+ chomp $line;
+ push @result, $line if length($line);
+ } until ( $line =~ /^---\s*END$/ or $socket->eof );
+ alarm 0;
+ };
+ my %return;
+ if ( $@ eq "timeout\n" ) {
+ return { error => 'request timed out' };
+ } elsif ( $@ ) {
+ return { error => $@ };
+ } else {
+ #+++ HLR9820 <date> <time>\n
+ # skip empty lines
+ my $header = shift(@result);
+ return { error => 'malformed response: '.$header }
+ unless $header =~ /^\+\+\+/;
+ $return{header} = $header;
+ #SMU #<serial number>\n
+ $return{smu} = shift(@result);
+ #%%<command string>%%\n
+ $return{echo} = shift(@result); # should match the input
+ #<message code>: <message description>\n
+ my $message = shift(@result);
+ if ($message =~ /^SUCCESS/) {
+ $return{success} = $message;
+ } else { #/^ERR/
+ $return{error} = $message;
+ }
+ $return{trailer} = pop(@result);
+ $return{details} = join("\n",@result,'');
+ }
+ \%return;
+}
+
+1;
--- /dev/null
+package FS::pay_batch::nacha;
+
+use strict;
+use vars qw( %import_info %export_info $name $conf );
+use Date::Format;
+#use Time::Local 'timelocal';
+#use FS::Conf;
+
+$name = 'NACHA';
+
+%import_info = (
+ #XXX stub finish me
+ 'filetype' => 'CSV',
+ 'fields' => [
+ ],
+ 'hook' => sub {
+ my $hash = shift;
+ },
+ 'approved' => sub { 1 },
+ 'declined' => sub { 0 },
+);
+
+%export_info = (
+
+ #optional
+ init => sub {
+ $conf = shift;
+ },
+
+ delimiter => '',
+
+
+ header => sub {
+ my( $pay_batch, $cust_pay_batch_arrayref ) = @_;
+
+ $conf->config('batchconfig-nacha-destination') =~ /^\s*(\d{9})\s*$/
+ or die 'illegal NACHA Destination';
+ my $dest = $1;
+
+ my $dest_name = $conf->config('batchconfig-nacha-destination_name');
+ $dest_name = substr( $dest_name. (' 'x23), 0, 23);
+
+ $conf->config('batchconfig-nacha-origin') =~ /^\s*(\d{10})\s*$/
+ or die 'illegal NACHA Origin';
+ my $origin = $1;
+
+ my $company = $conf->config('company_name', $pay_batch->agentnum);
+ $company = substr($company. (' 'x23), 0, 23);
+
+ my $now = time;
+
+ #haha don't want to break after a quarter million years of a batch a day
+ #or 54 years for 5000 agent-virtualized hosted companies batching daily
+ my $refcode = substr( (' 'x8). $pay_batch->batchnum, -8);
+
+ #or only 25,000 years or 5.4 for 5000 companies :)
+ #though they would probably want them numbered per company
+ my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7);
+
+ ##
+ # File Header Record
+ ##
+
+ '1'. #Record Type Code
+ '01'. #Priority Code
+ ' '. $dest. #Immediate Destination / 9-digit transit routing #
+ $origin. #Immediate Origin / 10 digit company number
+ time2str('%y%m%d', $now). #File Creation Date
+ time2str('%H%M', $now). #File Creation Time
+ 'A'. #XXX file ID modifier, mult. files in transit? [A-Z0-9]
+ '094'. #94 character records
+ '10'. #Blocking Factor
+ '1'. #Format code
+ $dest_name. #Immediate Destination Name / 23 char bank name
+ $company. #Immediate Origin Name / 23 char company name
+ $refcode. #Reference Code (internal/optional)
+
+ ###
+ # Batch Header Record
+ ###
+
+ '5'. #Record Type Code
+ '225'. #Service Class Code (220 credits only,
+ # 200 mixed debits & credits)
+ substr($company, 0, 16). #on cust. statements
+ (' 'x20 ). #20 char "company internal use if desired"
+ $origin. #Company Identification (Immediate Origin)
+ 'PPD'. #others?
+ #PPD "Prearranged Payments and Deposit entries" for consumer items
+ #CCD (Cash Concentration and Disbursement)
+ #CTX (Corporate Trade Exchange)
+ #TEL (Telephone initiated entires)
+ #WEB (Authorization received via the Internet)
+ 'InterntSvc'. #XXX from conf 10 char txn desc, printed on cust. statements
+
+ #6 char "Descriptive date" printed on customer statements
+ #XXX now? or use a separate post date?
+ time2str('%y%m%d', $now).
+
+ #6 char date transactions are to be posted
+ #XXX now? or do we need a future banking day date like eft_canada trainwreck
+ time2str('%y%m%d', $now).
+
+ (' 'x3). #Settlement Date / Reserved
+ '1'. #Originator Status Code
+ substr($dest, 0, 8). #Originating Financial Institution
+ $batchnum #Batch Number ("number batches sequentially")
+
+ },
+
+ 'row' => sub {
+ my( $cust_pay_batch, $pay_batch, $batchcount, $batchtotal ) = @_;
+
+ my ($account, $aba) = split('@', $cust_pay_batch->payinfo);
+
+ my $cust_main = $cust_pay_batch->cust_main;
+ my $cust_identifier = substr($cust_main->display_custnum. (' 'x15), 0, 15);
+
+ my $cust_name = substr($cust_main->name. (' 'x22), 0, 22);
+
+ #non-PPD transactions? future
+
+ ###
+ # PPD Entry Detail Record
+ ###
+
+ '6'. #Record Type Code
+ '27'. #27 checking debit, 37 savings debit XXX
+ $aba. #Receiving DFI Identification, check digit
+ substr($account.(' 'x17), 0, 17). #DFI Account number (Left justify)
+ sprintf('%010d', $cust_pay_batch->amount * 100). #Amount
+ $cust_identifier. #Individual Identification Number, 15 char
+ $cust_name. #Individual name (22-char)
+ ' '. #2 char "company internal use if desired"
+ '0'. #Addenda Record Indicator
+ (' 'x15) #15 digit "bank will assign trace number"
+ # (00000?)
+ },
+
+ 'footer' => sub {
+ my( $pay_batch, $batchcount, $batchtotal ) = @_;
+
+ $conf->config('batchconfig-nacha-destination') =~ /^\s*(\d{9})\s*$/
+ or die 'illegal NACHA Destination';
+ my $dest = $1;
+
+ $conf->config('batchconfig-nacha-origin') =~ /^\s*(\d{10})\s*$/
+ or die 'illegal NACHA Origin';
+ my $origin = $1;
+
+ my $batchnum = substr( ('0'x7). $pay_batch->batchnum, -7);
+
+ ###
+ # Batch Control Record
+ ###
+
+ '8'. #Record Type Code
+ '225'. #Service Class Code (220 credits only,
+ # 200 mixed debits&credits)
+ sprintf('%06d', $batchcount). #Entry / Addenda Count
+ '1234567890'. #XXX "Entry Hash" Total of all positions 4-11 on each 6 record. Only use the final 10 positions in the entry
+ sprintf('%012d', $batchtotal * 100). #Debit total
+ '000000000000'. #Credit total
+ $origin. #Company Identification (Immediate Origin)
+ (' 'x19). #Message Authentication Code (19 char blank)
+ (' 'x6). #Federal Reserve Use (6 char blank)
+ substr($dest, 0, 8). #Originating Financial Institution
+ $batchnum. #Batch Number ("number batches sequentially")
+
+ ###
+ # File Control Record
+ ###
+
+ '9'. #Record Type Code
+ '000001'. #Batch Counter (# of batch header recs)
+ sprintf('%06d', $batchcount + 4). #num of physical blocks on the file..?
+ sprintf('%08d', $batchcount). #total # of entry detail and addenda
+ '1234567890'. #XXX "Entry Hash" Total of all positions 4-11 on each 6 record. Only use the final 10 positions in the entry
+ sprintf('%012d', $batchtotal * 100). #Debit total
+ '000000000000'. #Credit total
+ ( ' 'x39 ) #Reserved / blank
+
+ },
+
+);
+
+1;
+
#!/usr/bin/perl
use strict;
-use vars qw( $opt_p );
+use vars qw( $opt_p $opt_t );
use Getopt::Std;
use FS::UID qw(adminsuidsetup);
use FS::Record qw(qsearchs);
use FS::cust_main;
+use FS::cust_tag;
-getopts('p:');
+getopts('p:t:');
my $user = shift or &usage;
adminsuidsetup $user;
next;
}
+ my %cust_tag = ( custnum=>$custnum, tagnum=>$opt_t );
+ if ( $opt_t && ! qsearchs('cust_tag', \%cust_tag) ) {
+ my $cust_tag = new FS::cust_tag \%cust_tag;
+ my $error = $cust_tag->insert;
+ die "$error\n" if $error;
+ }
+
if ( $opt_p ) {
$cust_main->payby($opt_p);
- }
- my $error = $cust_main->replace;
- die "$error\n" if $error;
+ my $error = $cust_main->replace;
+ die "$error\n" if $error;
+ }
}
sub usage {
- die "usage: cust_main-bulk_change -p NEW_PAYBY employee_username <custnums.txt\n";
+ die "usage: cust_main-bulk_change [ -p NEW_PAYBY ] [ -t tagnum ] employee_username <custnums.txt\n";
}
=head1 NAME
=head1 SYNOPSIS
- cust_main-bulk_change -p NEW_PAYBY username <custnums.txt
+ cust_main-bulk_change [ -p NEW_PAYBY ] [ -t tagnum ] username <custnums.txt
=head1 DESCRIPTION
-Command-line tool to change the payby field for a group of customers.
+Command-line tool to make bulk changes to a group of customers.
+
+-p: new payby, for example, I<CARD> or I<DCRD>
--p: new payby, for example, I<CARD> or I<DCRD>.
+-t: tagnum to add if not present
user: Employee username
{ field=>'by_default', type=>'checkbox', value=>'Y' },
$tagcolor,
],
- 'labels' => { 'tagnum' => 'Tag #',
+ 'labels' => { 'tagnum' => 'Tag',
'tagname' => 'Tag',
'tagdesc' => 'Message',
'tagcolor' => 'Highlight Color',