1 package Business::BatchPayment::BillBuddy;
7 Business::BatchPayment::BillBuddy - BillBuddy batch payment format and transport
11 See L<Business::BatchPayment> for general usage notes.
15 use Business::BatchPayment;
18 my @items = Business::BatchPayment::Item->new( ... );
19 my $batch = Business::BatchPayment->create(Batch =>
20 batch_id => $self->batchnum,
24 my $processor = Business::BatchPayment->processor('BillBuddy',
26 password => 'API_KEY',
27 host => 'xmlrpc.billbuddy.com',
34 my $result = $processor->submit($batch);
36 # this gets set by submit, and is needed for receive
37 my $processor_id = $batch->processor_id;
40 my @reply = $processor->receive(@process_ids);
42 =head2 PROCESSOR ATTRIBUTES
46 =item username - the user_id provided to you by BillBuddy
48 =item password - the api_key (NOT the web portal password) provided to you by BillBuddy
50 =item host - the domain name for BillBuddy XMLRPC requests
52 =item path - the path for BillBuddy XMLRPC requests
54 =item port - the port for BillBuddy XMLRPC requests (optional, default 443)
56 =item debug - print debug warnings if true, including XML requests and responses
62 Jonathan Prykop, jonathan@freeside.biz
66 You can find documentation for this module with the perldoc command.
68 perldoc Business::BatchPayment::BillBuddy
70 Commercial support is available from Freeside Internet Services,
71 L<http://www.freeside.biz>
73 =head1 LICENSE AND COPYRIGHT
75 Copyright 2015 Freeside Internet Services
77 This program is free software; you can redistribute it and/or modify it
78 under the terms of either: the GNU General Public License as published
79 by the Free Software Foundation; or the Artistic License.
81 See http://dev.perl.org/licenses/ for more information.
85 use Business::BatchPayment;
88 with 'Business::BatchPayment::Processor';
90 our $VERSION = '0.03';
92 has [ qw(username password) ] => (
100 default => 'xmlrpc.billbuddy.com',
115 sub default_transport {
117 Business::BatchPayment->create('BillBuddy::Transport',
118 username => $self->username,
119 password => $self->password,
123 debug => $self->debug,
128 my ($self,$item,$batch) = @_;
129 #Position Length Content
132 #2-17 16 Reference Number
133 $line .= sprintf("%-16s",$item->tid);
134 #18-18 1 blank, filled with space
136 #19-28 10 amount, numbers only, by cents, zero padded to the left
137 $line .= sprintf("%010s",$item->amount * 100);
138 #29-30 2 blank, filled with spaces
140 #31-32 2 account type: "BC" for bank account, "CC" for credit card account
141 my $pt = $item->payment_type;
143 #we currently don't support CC, but leaving the code in place for future development
144 die 'Business::BatchPayment::BillBuddy currently does not handle credit card transactions';
146 } elsif ($pt eq 'ECHECK') {
149 die "Unknown payment type";
153 #34-40 7 BSB for bank account, formatted in 000-000. blank for credit card account
154 my $bsb = ($pt eq 'CC') ? sprintf("%7s",'') : $item->routing_code;
155 $bsb =~ s/^(\d{3})(\d{3})/$1\-$2/;
156 die "Bad routing code $bsb" if ($pt ne 'CC') && ($bsb !~ /^\d{3}\-\d{3}$/);
160 #42-50 9 Account number for bank accounts. blank for credit card account
161 my $anum = ($pt eq 'CC') ? sprintf("%9s",'') : sprintf("%09s",$item->account_number);
163 #51-66 16 credit card number, left padded with zero if less than 16 digits. Blank for bank accounts
164 my $cnum = ($pt eq 'CC') ? sprintf("%016s",$item->card_number) : sprintf("%16s",'');
166 #67-98 32 bank account name or name on the credit card
167 my $name = $item->first_name . ' ' . $item->last_name;
168 $name =~ s/\'//g; # gateway should be handling this, but it's not
169 $line .= sprintf("%-32.32s",$name);
172 #100-103 4 credit card expiry date, formatted as mmdd. "0000" for bank account.
173 my $exp = ($pt eq 'CC') ? $item->expiration : '';
174 $line .= sprintf("%04s",$exp);
176 #105-111 7 reserved, always "0000000"
177 #112-114 3 reserved, blank
178 $line .= ' 0000000 ';
179 #115-120 6 line number, left padded with zero
180 $line .= sprintf("%06s",$batch->num);
185 #overriding this just to be able to pass batch to upload
186 #but maybe this should go in standard module?
190 my $request = $self->format_request($batch);
191 $self->transport->upload($request,$batch);
194 #overriding this to pass process_ids to download,
195 #but maybe this should go in standard module?
198 return $self->transport->download(@_);
201 package Business::BatchPayment::BillBuddy::Transport;
203 use XML::Simple qw(:strict);
207 extends 'Business::BatchPayment::Transport::HTTPS';
209 has [ qw(username password) ] => (
227 # this is really specific to BillBuddy, not a generic XML formatting routine
229 my ($self,$sid,@param) = @_;
231 my $xml = XML::Writer->new(
235 $xml->startTag('postdata');
236 $xml->dataElement('sessionid',$sid);
237 $xml->dataElement('clientidentifier','');
238 $xml->startTag('parameters');
239 foreach my $param (@param) {
240 if (ref($param) eq 'ARRAY') {
241 my $type = $$param[0];
242 my $value = $$param[1];
243 $xml->$type('parameter',$value);
245 $xml->dataElement('parameter',$param);
248 $xml->endTag('parameters');
249 $xml->endTag('postdata');
254 # also specific to BillBuddy, doesn't actually follow XMLRPC standard for response
256 my ($self,$func,$sid,@param) = @_;
257 my $path = $self->path;
258 $path = '/' . $path unless $path =~ /^\//;
259 $path .= '/' unless $path =~ /\/$/;
261 my $xmlcontent = $self->xml_format($sid,@param);
262 warn $self->host . ' ' . $self->port . ' ' . $path . "\n" . $xmlcontent if $self->debug;
263 my ($response, $rcode, %rheaders) = $self->https_post($path,$xmlcontent);
264 die "Bad response from gateway: $rcode\n" unless $rcode eq '200 OK';
265 warn $response . "\n" if $self->debug;
266 my $rref = XMLin($response, KeyAttr => ['ResponseData'], ForceArray => []);
267 die "Error from gateway: " . $rref->{'ResponseStatusDescription'}. "\n"
268 if $rref->{'ResponseStatus'};
272 #gets date from batch & sets processor_id in batch
274 my ($self,$request,$batch) = @_;
277 my $resp = $self->xmlrpc_post('xmlrpc_tp_Login.asp','',$self->username,$self->password);
278 my $sid = $resp->{'ResponseData'}->{'sessionID'};
279 die "Could not parse sessionid from gateway response" unless $sid;
280 # get date from login, to ensure we're using upstream date
281 my ($year,$mon,$mday,$hour,$min,$sec) = $resp->{'ResponseTimestamp'} =~ /^(....)-(..)-(..)\s+(..):(..):(..)/;
282 # then add a day and a bit, because "processs date need to be a date in the future"
283 my $date = DateTime->new(
290 # timezone on object mostly doesn't matter,
291 # but this does appear to be the tz being passed by BillBuddy,
292 # and this should avoid DST troubles (Queensland does not do DST)
293 time_zone => 'Australia/Queensland',
295 # extra hour is buffer for upload to run, hopefully that's plenty
296 DateTime::Duration->new( hours => 25 )
298 # start a payment batch
299 $resp = $self->xmlrpc_post('xmlrpc_tp_DDRBatch_Open.asp',$sid,$self->username,$date);
300 my $batchno = $resp->{'ResponseData'}->{'batchno'};
301 die "Could not parse batchno from gateway response" unless $batchno;
302 $batch->processor_id($batchno);
303 # post a payment transaction
304 foreach my $line (split(/\n/,$request)) {
305 $self->xmlrpc_post('xmlrpc_tp_DDRTransaction_Add.asp',$sid,$self->username,$batchno,['cdataElement',$line]);
307 # close payment batch
308 $self->xmlrpc_post('xmlrpc_tp_DDRBatch_Close.asp',$sid,$self->username,$batchno);
309 # submit payment batch
310 $self->xmlrpc_post('xmlrpc_tp_DDRBatch_Submit.asp',$sid,$self->username,$batchno);
312 $self->xmlrpc_post('xmlrpc_tp_Logout.asp',$sid,$self->username);
316 # caution--this method developed without access to completed test payments
317 # built with best guesses, cross your fingers...
320 my @processor_ids = @_;
321 return () unless @processor_ids;
323 my $resp = $self->xmlrpc_post('xmlrpc_tp_Login.asp','',$self->username,$self->password);
324 my $sid = $resp->{'ResponseData'}->{'sessionID'};
325 die "Could not parse sessionid from gateway response" unless $sid;
327 foreach my $batchno (@processor_ids) {
328 #get BillBuddy transaction ids for batch
329 $resp = $self->xmlrpc_post('xmlrpc_tp_DDRBatch_getTranList.asp',$sid,$self->username,$batchno);
330 my $tids = $resp->{'ResponseData'}->{'id'};
331 next unless $tids; #error/die instead?
333 $tids = ref($tids) ? $tids : [ $tids ];
334 #get status by individual transaction
335 foreach my $tid (@$tids) {
336 $resp = $self->xmlrpc_post('xmlrpc_tp_DDRBatch_getTranStatus.asp',$sid,$self->username,$tid);
337 my $status = lc($resp->{'ResponseData'}->{'bankprocessstatus'});
339 next if grep(/^$status$/,('submitted','processing','scheduled'));
340 $error = "Unknown return status: $status"
341 unless grep(/^$status$/,('approved','deleted','declined'));
342 my $item = Business::BatchPayment->create(Item =>
343 order_number => $tid,
344 tid => $resp->{'ResponseData'}->{'referencenumber'},
345 approved => ($status eq 'approved') ? 1 : 0,
346 error_message => $error,
349 if ($resp->{'ResponseData'}->{'actualprocessdate'} =~ /^(\d\d\d\d).(\d\d).(\d\d)/) {
355 # this appears to be the tz being passed by BillBuddy
356 time_zone => 'Australia/Queensland',
360 warn "Could not parse actualprocessdate ".$resp->{'ResponseData'}->{'actualprocessdate'};
362 push(@batchitems,$item);
365 push(@batches, Business::BatchPayment->create('Batch', items => \@batchitems));
369 $self->xmlrpc_post('xmlrpc_tp_Logout.asp',$sid,$self->username);