1 package Business::BatchPayment::BillBuddy;
5 use Business::BatchPayment;
7 with 'Business::BatchPayment::Processor';
11 has [ qw(user_id api_key host) ] => (
29 sub default_transport {
31 Business::BatchPayment->create('BillBuddy::Transport',
32 user_id => $self->user_id,
33 api_key => $self->api_key,
37 debug => $self->debug,
42 my ($self,$item,$batch) = @_;
43 #Position Length Content
46 #2-17 16 Reference Number
47 $line .= sprintf("%-16s",$item->tid);
48 #18-18 1 blank, filled with space
50 #19-28 10 amount, numbers only, by cents, zero padded to the left
51 $line .= sprintf("%10s",$item->amount * 100);
52 #29-30 2 blank, filled with spaces
54 #31-32 2 account type: "BC" for bank account, "CC" for credit card account
55 my $pt = $item->payment_type;
57 #we currently don't support CC, but leaving the code in place for future development
58 die 'Business::BatchPayment::BillBuddy currently does not handle credit card transactions';
60 } elsif ($pt eq 'ECHECK') {
63 die "Unknown payment type";
67 #34-40 7 BSB for bank account, formatted in 000-000. blank for credit card account
68 my $bsb = ($pt eq 'CC') ? sprintf("%7s",'') : $item->routing_code;
69 $bsb =~ s/^(\d{3})(\d{3})/$1\-$2/;
70 die "Bad routing code $bsb" if ($pt ne 'CC') && ($bsb !~ /^\d{3}\-\d{3}$/);
74 #42-50 9 Account number for bank accounts. blank for credit card account
75 my $anum = ($pt eq 'CC') ? sprintf("%9s",'') : sprintf("%09s",$item->account_number);
77 #51-66 16 credit card number, left padded with zero if less than 16 digits. Blank for bank accounts
78 my $cnum = ($pt eq 'CC') ? sprintf("%016s",$item->card_number) : sprintf("%16s",'');
80 #67-98 32 bank account name or name on the credit card
81 my $name = $item->first_name . ' ' . $item->last_name;
82 $line .= sprintf("%-32.32s",$name);
85 #100-103 4 credit card expiry date, formatted as mmdd. "0000" for bank account.
86 my $exp = ($pt eq 'CC') ? $item->expiration : '';
87 $line .= sprintf("%04s",$exp);
89 #105-111 7 reserved, always "0000000"
90 #112-114 3 reserved, blank
92 #115-120 6 line number, left padded with zero
93 $line .= sprintf("%06s",$batch->num);
98 #overriding this just to be able to pass batch to upload
99 #but maybe this should go in standard module?
103 my $request = $self->format_request($batch);
104 warn $request if $self->debug >= 2;
105 $self->transport->upload($request,$batch);
108 #overriding this to pass process_ids to download,
109 #but maybe this should go in standard module?
112 return $self->transport->download(@_);
115 package Business::BatchPayment::BillBuddy::Transport;
117 use XML::Simple qw(:strict);
121 extends 'Business::BatchPayment::Transport::HTTPS';
123 has [ qw(user_id api_key) ] => (
141 # this is really specific to BillBuddy, not a generic XML formatting routine
143 my ($self,$sid,@param) = @_;
145 my $xml = XML::Writer->new(
149 $xml->startTag('postdata');
150 $xml->dataElement('sessionid',$sid);
151 $xml->dataElement('clientidentifier','');
152 $xml->startTag('parameters');
153 foreach my $param (@param) {
154 if (ref($param) eq 'ARRAY') {
155 my $type = $$param[0];
156 my $value = $$param[1];
157 $xml->$type('parameter',$value);
159 $xml->dataElement('parameter',$param);
162 $xml->endTag('parameters');
163 $xml->endTag('postdata');
168 # also specific to BillBuddy, doesn't actually follow XMLRPC standard for response
170 my ($self,$func,$sid,@param) = @_;
171 my $path = $self->path;
172 $path = '/' . $path unless $path =~ /^\//;
173 $path .= '/' unless $path =~ /\/$/;
175 my $xmlcontent = $self->xml_format($sid,@param);
176 warn $self->host . ' ' . $self->port . ' ' . $path . "\n" . $xmlcontent if $self->debug;
177 my ($response, $rcode, %rheaders) = $self->https_post($path,$xmlcontent);
178 die "Bad response from gateway: $rcode" unless $rcode eq '200 OK';
179 warn $response . "\n" if $self->debug;
180 my $rref = XMLin($response, KeyAttr => ['ResponseData'], ForceArray => []);
181 die "Error from gateway: " . $rref->{'ResponseStatusDescription'} if $rref->{'ResponseStatus'};
185 #gets date from batch & sets processor_id in batch
187 my ($self,$request,$batch) = @_;
189 # get date from batch
190 my ($date) = $batch->process_date =~ /^(....-..-..)/;
192 my $resp = $self->xmlrpc_post('xmlrpc_tp_Login.asp','',$self->user_id,$self->api_key);
193 my $sid = $resp->{'ResponseData'}->{'sessionID'};
194 die "Could not parse sessionid from gateway response" unless $sid;
195 # start a payment batch
196 $resp = $self->xmlrpc_post('xmlrpc_tp_DDRBatch_Open.asp',$sid,$self->user_id,$date);
197 my $batchno = $resp->{'ResponseData'}->{'batchno'};
198 die "Could not parse batchno from gateway response" unless $batchno;
199 $batch->processor_id($batchno);
200 # post a payment transaction
201 foreach my $line (split(/\n/,$request)) {
202 $self->xmlrpc_post('xmlrpc_tp_DDRTransaction_Add.asp',$sid,$self->user_id,$batchno,['cdataElement',$line]);
204 # close payment batch
205 $self->xmlrpc_post('xmlrpc_tp_DDRBatch_Close.asp',$sid,$self->user_id,$batchno);
206 # submit payment batch
207 $self->xmlrpc_post('xmlrpc_tp_DDRBatch_Submit.asp',$sid,$self->user_id,$batchno);
209 $self->xmlrpc_post('xmlrpc_tp_Logout.asp',$sid,$self->user_id);
213 # caution--this method developed without access to completed test payments
214 # built with best guesses, cross your fingers...
217 my @processor_ids = @_;
218 return () unless @processor_ids;
220 my $resp = $self->xmlrpc_post('xmlrpc_tp_Login.asp','',$self->user_id,$self->api_key);
221 my $sid = $resp->{'ResponseData'}->{'sessionID'};
222 die "Could not parse sessionid from gateway response" unless $sid;
224 foreach my $batchno (@processor_ids) {
225 #get BillBuddy transaction ids for batch
226 $resp = $self->xmlrpc_post('xmlrpc_tp_DDRBatch_getTranList.asp',$sid,$self->user_id,$batchno);
227 my $tids = $resp->{'ResponseData'}->{'id'};
228 next unless $tids; #error/die instead?
230 $tids = ref($tids) ? $tids : [ $tids ];
231 #get status by individual transaction
232 foreach my $tid (@$tids) {
233 $resp = $self->xmlrpc_post('xmlrpc_tp_DDRBatch_getTranStatus.asp',$sid,$self->user_id,$tid);
234 my $status = lc($resp->{'ResponseData'}->{'bankprocessstatus'});
236 next if grep(/^$status$/,('submitted','processing','scheduled'));
237 $error = "Unknown return status: $status"
238 unless grep(/^$status$/,('deleted','declined'));
239 my $item = Business::BatchPayment->create(Item =>
240 order_number => $tid,
241 tid => $resp->{'ResponseData'}->{'referencenumber'},
242 approved => ($status eq 'approved') ? 1 : 0,
243 error_message => $error,
246 #not sure what format date gets returned in, item creation will fail on bad format,
247 #so I'm taking a guess, and not recording the date if my guess is wrong
248 if ($resp->{'ResponseData'}->{'actualprocessdate'} =~ /^(\d\d\d\d).(\d\d).(\d\d)/) {
249 $item->payment_date($1.'-'.$2.'-'.$3);
251 push(@batchitems,$item);
254 push(@batches, Business::BatchPayment->create('Batch', items => \@batchitems));
258 $self->xmlrpc_post('xmlrpc_tp_Logout.asp',$sid,$self->user_id);