1 package Business::BatchPayment::TD_EFT;
10 Business::BatchPayment::TD_EFT - TD Commercial Banking EFT1464 batch format
18 See L<Business::BatchPayment> for general usage notes.
22 use Business::BatchPayment;
24 my @items = Business::BatchPayment::Item->new( ... );
26 my $processor = Business::BatchPayment->processor('TD_EFT',
27 originator => 'TDABC22334',
28 datacentre => '00400',
29 short_name => 'AcmeCorp',
30 long_name => 'The Acme Corporation',
31 return_branch => '10202', # 0004 + 5-digit branch number
32 return_account => '00124598951', # 11 digits
34 # optional, for SFTP file transport
35 host => '192.168.100.1',
37 password => 'mypassword',
38 # optional, for encryption
39 encrypt_cmd => 'compx ASCII DE3',
40 decrypt_cmd => 'decompx',
41 encrypt_key => '/path/to/encrypt.key',
44 my $result = $processor->submit(@items);
48 Requires L<Net::SFTP::Foreign> and ssh (for file transfer).
50 =head2 PROCESSOR ATTRIBUTES
54 =item originator - Originator ID, 10 characters
56 =item datacentre - Your TD datacentre number, 5 digits
58 =item short_name - Your company name in 15 characters or less.
60 =item long_name - Your company name in 30 characters or less.
62 =item return_account, return_branch - The account number and bank branch
63 to use for returned payments.
65 =item cpa_code - Your 3-digit CPA industry code.
69 =head2 TRANSPORT ATTRIBUTES
71 These need to be specified only if you're using the SFTP transport.
75 =item host - SFTP hostname
77 =item login - SFTP username
79 =item password - SFTP password
81 =item encrypt_cmd - Command to use to encrypt/compress batches before
82 sending. This will be called, somewhat awkwardly, with the name of the
83 output file inserted as the first argument, and the name of the input
84 file (the plaintext) passed on standard input. If empty, batches will
87 =item descrypt_cmd - Command to decrypt/decompress downloaded batches.
89 =item encrypt_key - File to copy into the working directory before
90 running encrypt/decrypt commands. COMPX requires the encryption key
91 to be supplied like this.
101 use Moose::Util::TypeConstraints;
103 with 'Business::BatchPayment::Processor';
105 has [ qw( originator short_name long_name
106 return_branch return_account cpa_code ) ] => (
112 has 'datacentre' => (
114 isa => enum([qw( 00400 00410 00420 00430 00470 00490)]),
118 has [ qw( login password host encrypt_cmd encrypt_key decrypt_cmd ) ] =>
123 ); # only required for the default transport
127 # should be a parametric string type or something
128 $self->originator( sprintf('%-10.10s', $self->originator) );
129 $self->short_name( sprintf('%-15.15s', $self->short_name) );
130 $self->long_name( sprintf('%-30.30s', $self->long_name) );
131 $self->return_account( sprintf('%-11.11s', $self->return_account) . ' ' );
132 $self->return_branch ( '0004'.
133 sprintf('%-5.5s', $self->return_branch) )
134 unless length($self->return_branch) == 9;
137 sub default_transport {
139 Business::BatchPayment->create('TD_EFT::Transport' =>
140 login => $self->login,
141 password => $self->password,
143 put_path => 'APXBA807/',
144 debug => $self->debug,
145 encrypt_cmd => $self->encrypt_cmd,
146 decrypt_cmd => $self->decrypt_cmd,
147 encrypt_key => $self->encrypt_key,
152 before format_request => sub {
153 my ($self, $batch) = @_;
155 Business::BatchPayment::TD_EFT::Batch->meta->apply($batch);
157 my $dt = DateTime->now;
158 $dt->set_time_zone('local');
159 $batch->create_date(sprintf('%03d%03d', $dt->year % 1000, $dt->day_of_year));
161 my $counter = $batch->batch_id;
162 $counter = 0 unless $counter =~ /^\d+$/;
163 # a number from 1 to 9999
164 $batch->fcn(sprintf('%04u', ($counter % 9999) + 1));
165 # We can't return the FCN as the batch_id because it wraps around.
166 # The TIDs are still correct though.
170 my ($self, $batch) = @_;
174 '000000001' . #row number
177 $batch->create_date .
179 sprintf('%-1464s', $header);
183 my ($self, $item, $batch) = @_;
185 $batch->row($batch->row + 1);
186 # Avoid floating point error: if we're passed a non-exact number of
187 # cents (and we will be...), round to the nearest integer, and then
188 # sum integer numbers of cents to get the batch total.
189 my $cents = sprintf('%.0f',$item->amount * 100);
190 if ( $item->action eq 'payment' ) {
191 $batch->total_payment( $batch->total_payment + $cents );
192 $batch->count_payment( $batch->count_payment + 1 );
193 } elsif ( $item->action eq 'credit' ) {
194 $batch->total_credit( $batch->total_credit + $cents );
195 $batch->count_credit( $batch->count_credit + 1 );
198 # default to tomorrow
199 # (should this use Time::Business? Date::Holidays::CA?)
200 # (should we just require the merchant to specify a process date?)
201 my $process_date = $item->process_date;
202 if ( $process_date ) {
203 $process_date->set_time_zone('local');
205 $process_date = DateTime->today->set_time_zone('local')->add(days => 1);
208 my $duedate = sprintf('%03d%03d',
209 $process_date->year % 1000,
210 $process_date->day_of_year);
211 # The 1464 byte format supports up to 5 payments per line, but we're
214 ($item->action eq 'credit' ? 'C' : 'D') .
215 sprintf('%09u', $batch->row) .
219 sprintf('%010u', $cents) .
221 sprintf('%09u', $item->routing_code) .
222 sprintf('%-12.12s', $item->account_number ) .
226 sprintf('%-30.30s', $item->first_name . ' ' . $item->last_name) .
229 sprintf('%-19.19s', $item->tid) . #originator reference number
230 $self->return_branch .
231 $self->return_account .
232 ' ' x (15 + 22 + 2) .
235 sprintf('%-1464s', $row);
239 my ($self, $batch) = @_;
242 sprintf('%09u', $batch->row + 1) .
245 sprintf('%014u', $batch->total_payment) .
246 sprintf('%08u', $batch->count_payment) .
247 sprintf('%014u', $batch->total_credit) .
248 sprintf('%08u', $batch->count_credit)
250 sprintf('%-1464s', $trailer);
254 # two different response formats to consider
255 my ($self, $response) = @_;
256 my $batch = Business::BatchPayment->create('Batch');
257 Business::BatchPayment::TD_EFT::Batch->meta->apply($batch);
259 my @rows = split("\n", $response);
260 if ( length($rows[0]) == 80 and $rows[0] =~ /^H/ ) {
261 return $self->parse_ret_80($batch, @rows);
262 } elsif ( length($rows[0]) == 264 and $rows[0] =~ /^A/ ) {
263 return $self->parse_ack_264($batch, @rows);
265 die "can't determine format of file starting with\n$rows[0]\n";
270 # for error messages in the ACK file
275 'File Creation Number',
279 'Financial Institution ID',
280 'Payor/Payee Account Number',
283 'Originator Short Name',
285 'Originator Long Name',
287 'Originator Reference',
288 'Return Branch Number',
289 'Return Account Number',
297 # TD EFT 264-byte acknowledgement file
298 # One of these is sent for every batch. It reports any records
299 # that failed format screening.
300 my ($self, $batch, @rows) = @_;
301 warn "Acknowledgement file" if $self->debug;
303 foreach my $row (@rows) {
305 if ( $row =~ /^A/ ) {
307 $row =~ /^(.{1})(.{9})(.{10})(.{4})(.{6})(.{5})(.{7})(.{10})(.{7})(.{10})(.{5})(.{1})(.{30})( {159})$/ or die "invalid header row\n";
308 # Most of these fields aren't interesting to us.
310 $batch->create_date($5);
312 $date =~ /^(....)-(..)-(..)$/; # actual process date, YYYY-MM-DD
313 $payment_date = DateTime->new(year => $1, month => $2, day => $3);
314 } elsif ( $row =~ /^[CD]/ ) {
315 # Rejected item detail.
316 my @f = ($row =~ /^(.{1})(.{9})(.{14})(.{3})(.{10})(.{6})(.{9})(.{12})(.{22})(.{3})(.{15})(.{30})(.{30})(.{10})(.{19})(.{9})(.{12})(.{15})(.{22})(.{2})(.{11})$/)
317 or die "invalid detail row\n";
318 foreach (@f) { s/^\s+//; s/\s+$//; }
319 unshift @f, ''; # make field numbers line up
321 if ( $f[1] eq 'C' ) { $action = 'credit' }
322 elsif ( $f[1] eq 'D' ) { $action = 'payment' }
325 my $item = Business::BatchPayment->create('Item' =>
327 payment_type => 'ECHECK',
329 amount => sprintf('%.2f', $f[5] / 100),
330 payment_date => $payment_date,
331 routing_code => $f[7],
332 account_number => $f[8],
335 my @error_fields = map { $field_order[$_] } ($f[21] =~ /../g) if $f[21];
336 $item->error_message('invalid fields: '.join(', ', @error_fields));
338 } elsif ( $row =~ /^R/ ) {
339 # um...this indicates the whole batch was rejected
340 # not quite sure what to do with that
341 $row =~ /^(.{1})(.{9})(.{10})(.{4})(.{5})(.{75})( {160})$/;
343 die "batch rejected: $5 $6\n";
349 $self->parse_error($row, $_);
351 die "no valid header row found\n" unless $payment_date;
356 my %return_reason = (
357 '00' => 'Edit Reject',
358 '01' => 'Insufficient Funds',
359 '02' => 'Cannot Trace',
360 '03' => 'Payment Stopped/Recalled',
361 '04' => 'Post/Stale Dated',
362 '05' => 'Account Closed',
363 '06' => 'Account Transferred',
364 '07' => 'No Chequing Privileges',
365 '08' => 'Funds Not Cleared',
367 '10' => 'Payor/Payee Deceased',
368 '11' => 'Account Frozen',
369 '12' => 'Invalid/Incorrect Account Number',
370 '13' => 'Contact Payor/Payee',
371 '14' => 'Incorrect Payor/Payee Name',
372 '15' => 'Refused by Payor/Payee',
373 '80' => 'Payment Recalled',
377 # TD EFT 80 byte returned items file
378 # This reports unpostable and dishonored payments.
379 # This may be sent at any time after receiving the request. If there
380 # are no dishonored payments, no file will be sent.
381 my ($self, $batch, @rows) = @_;
382 warn "Returned item notification" if $self->debug;
384 foreach my $row (@rows) {
386 if ( $row =~ /^H/ ) {
388 $row =~ /^(.{1})(.{10})(.{1})( {3})(.{6})(.{30})(.{9})(.{12})( {8})$/
389 or die "invalid header row\n";
390 # the only field we care about is payment vs. credit
391 # and even that only minimally
394 } elsif ( $3 eq 'J') {
397 } elsif ( $row =~ /^D/ ) {
399 my @f = ( $row =~ /^(.{1})(.{20})(.{2})(.{1})(.{6})(.{19})(.{9})(.{12})(.{10})$/ )
400 or die "invalid detail row\n";
401 foreach (@f) { s/^\s+//; s/\s+$//; }
403 my $item = Business::BatchPayment->create('Item' =>
405 payment_type => 'ECHECK',
407 amount => sprintf('%.2f', $f[9] / 100),
408 routing_code => $f[7],
409 account_number => $f[8],
411 error_message => "$f[3] ".$return_reason{$f[3]},
416 # not interesting, though we could use it as an error check
419 $self->parse_error($row, $_);
421 die "no valid header row found\n" unless $action;
426 package Business::BatchPayment::TD_EFT::Batch;
428 use List::Util qw(sum);
430 has [qw( create_date fcn )] => ( is => 'rw', isa => 'Str' );
431 # XXX use the "totals" method instead
432 has [qw( row total_payment total_credit count_payment count_credit )] =>
433 ( is => 'rw', isa => 'Int', default => 0 );
437 Mark Wells, C<< <mark at freeside.biz> >>
443 You can find documentation for this module with the perldoc command.
445 perldoc Business::BatchPayment::TD_EFT
447 Commercial support is available from Freeside Internet Services, Inc.
449 L<http://www.freeside.biz>
451 =head1 ACKNOWLEDGEMENTS
453 =head1 LICENSE AND COPYRIGHT
455 Copyright 2012 Mark Wells.
457 This program is free software; you can redistribute it and/or modify it
458 under the terms of either: the GNU General Public License as published
459 by the Free Software Foundation; or the Artistic License.
461 See http://dev.perl.org/licenses/ for more information.
466 1; # End of Business::BatchPayment::TD_EFT