File Coverage

blib/lib/Business/NAB/Types.pm
Criterion Covered Total %
statement 42 42 100.0
branch 9 10 90.0
condition 5 6 83.3
subroutine 12 12 100.0
pod 1 2 50.0
total 69 72 95.8


line stmt bran cond sub pod time code
1             package Business::NAB::Types;
2             $Business::NAB::Types::VERSION = '0.03';
3             =head1 NAME
4              
5             Business::NAB::Types
6              
7             =head1 SYNOPSIS
8              
9             use Business::NAB::Types qw/
10             add_max_string_attribute
11             /;
12              
13             has [ qw/
14             process_date
15             / ] => (
16             is => 'ro',
17             isa => 'NAB::Type::Date',
18             required => 1,
19             coerce => 1,
20             );
21              
22             ...
23              
24             =head1 DESCRIPTION
25              
26             Package for defining type constraints for use in the Business::NAB
27             namespace. All types are namespaced to C<NAB::Type::*>.
28              
29             =cut
30              
31 29     29   231 use strict;
  29         87  
  29         1270  
32 29     29   167 use warnings;
  29         61  
  29         2083  
33 29     29   229 use feature qw/ signatures /;
  29         137  
  29         4690  
34              
35 29     29   261 use Moose::Util::TypeConstraints;
  29         78  
  29         358  
36 29     29   76011 no warnings qw/ experimental::signatures /;
  29         78  
  29         1837  
37              
38 29     29   17046 use DateTime::Format::DateParse; ## no critic
  29         1020356  
  29         1785  
39             use Exporter::Easy (
40 29         287 OK => [
41             qw/
42             add_max_string_attribute
43             decamelize
44             /
45             ]
46 29     29   17925 );
  29         63345  
47              
48             =head1 TYPES
49              
50             =over
51              
52             =item NAB::Type::Date
53              
54             A DateTime object, this will be coerced from the string DDMMYY or DDMMYYYY
55              
56             =cut
57              
58             class_type 'DateTime';
59              
60             subtype 'NAB::Type::Date'
61             => as 'DateTime';
62              
63             coerce 'NAB::Type::Date'
64             => from 'Str'
65             => via {
66             my $date_str = $_;
67              
68             return $date_str if ref( $date_str );
69              
70             if ( $date_str =~ /^(\d{2})(\d{2})(\d{2,4})$/ ) {
71             my ( $dd, $mm, $yy ) = ( $1, $2, $3 );
72             my $yyyy = length( $yy ) == 4 ? $yy : "20$yy"; # Y2K never happened?
73             $date_str = "$yyyy-$mm-$dd";
74             }
75              
76             return DateTime::Format::DateParse->parse_datetime( $date_str );
77             };
78              
79             =item NAB::Type::StatementDate
80              
81             A DateTime object, this will be coerced from the string YYMMDD
82              
83             =cut
84              
85             subtype 'NAB::Type::StatementDate'
86             => as 'DateTime';
87              
88             coerce 'NAB::Type::StatementDate'
89             => from 'Str'
90             => via {
91             my $date_str = $_;
92              
93             return $date_str if ref( $date_str );
94              
95             my ( $yy, $mm, $dd ) = ( $date_str =~ /^(\d{2,4})(\d{2})(\d{2})$/ );
96             my $yyyy = length( $yy ) == 4 ? $yy : "20$yy"; # Y2K never happened?
97             return DateTime::Format::DateParse->parse_datetime( "$yyyy-$mm-$dd" );
98             };
99              
100             =item NAB::Type::BRFInt
101              
102             =cut
103              
104             subtype 'NAB::Type::BRFInt'
105             => as 'Int'
106             ;
107              
108             coerce 'NAB::Type::BRFInt'
109             => from 'Str'
110             => via {
111             my $str = $_;
112              
113             # trailer record amounts in BPAY Remittance Files use the last
114             # character to represent:
115             # - the last digit
116             # - the sign
117             #
118             # so we convert that to an actual signed integer here
119             # see also: Business::NAB::BPAY::Remittance::File::TrailerRecord
120             # sub _brf_int
121             if ( $str =~ /[{A-I]$/ ) {
122             $str =~ tr/{A-I/0-9/;
123             } elsif ( $str =~ /[}J-R]$/ ) {
124             $str =~ tr/}J-R/0-9/;
125             $str *= -1;
126             }
127              
128             return $str;
129             };
130              
131             =item NAB::Type::PositiveInt
132              
133             An Int greater than zero
134              
135             =cut
136              
137             subtype 'NAB::Type::PositiveInt'
138             => as 'Int'
139             => where { $_ > 0 }
140             => message { "The number provided, $_, was not positive" }
141             ;
142              
143             =item NAB::Type::PositiveIntOrZero
144              
145             An Int greater than or equal to zero
146              
147             =cut
148              
149             subtype 'NAB::Type::PositiveIntOrZero'
150             => as 'Int'
151             => where { $_ >= 0 }
152             => message { "The number provided, $_, was not positive or zero" }
153             ;
154              
155             =item NAB::Type::BSBNumber
156              
157             =item NAB::Type::BSBNumberNoDash
158              
159             A Str of the form C</^\d{3}-\d{3}$/>
160              
161             A Str of the form C</^\d{6}$/>
162              
163             Some file formats for NAB require a BSB with the dash, while other require
164             the BSB without the dash. This is a hard requirement and files will be
165             rejected if you fail to handle it.
166              
167             The types here are defined so that you can pass either format in and they
168             will be coerced to the correct format for the file type in question.
169              
170             =cut
171              
172             subtype 'NAB::Type::BSBNumber'
173             => as 'Str',
174             => where { $_ =~ /^\d{3}-\d{3}$/ }
175             => message { "The BSB provided, $_, does not match \\d{3}-\\d{3}" }
176             ;
177              
178             subtype 'NAB::Type::BSBNumberNoDash'
179             => as 'Str',
180             => where { $_ =~ /^\d{6}$/ }
181             => message { "The BSB provided, $_, does not match \\d{6}" }
182             ;
183              
184             coerce 'NAB::Type::BSBNumber'
185             => from 'Str'
186             => via {
187              
188             # NAB require the - char, ensure it's there
189             $_ =~ s/^(\d{3})(\d{3})/$1-$2/;
190             return $_;
191             };
192              
193             coerce 'NAB::Type::BSBNumberNoDash'
194             => from 'Str'
195             => via {
196              
197             # NAB don't require the - char, ensure it's not there
198             $_ =~ s/^(\d{3})-(\d{3})/$1$2/;
199             return $_;
200             };
201              
202              
203             =item NAB::Type::AccountNumber
204              
205             A Str of the form:
206              
207             * Alpha-Numeric (A-z0-9)
208             * Hyphens & blanks only are valid
209             * Must not contain all blanks or all zeros
210              
211             And:
212              
213             * Leading zeros, which are part of an account number, must be shown
214             * Edit out hyphens where account number exceeds nine characters
215             * Right justified
216             * Leave blank
217              
218             =cut
219              
220             subtype 'NAB::Type::AccountNumber'
221             => as 'Str',
222             => where {
223             length( $_ ) < 10
224             && $_ =~ /^[A-z0-9\ \-]+$/
225             && $_ !~ /^(\s|0)+$/
226             }
227             => message { "The account number provided, $_, is not valid" }
228             ;
229              
230             =item NAB::Type::Indicator
231              
232             A Str of the form C</^[\ NTWXY]$/>
233              
234             =cut
235              
236             subtype 'NAB::Type::Indicator'
237             => as 'Maybe[Str]',
238             => where { $_ =~ /^[ NTWXY]$/ }
239             => message { "The indicator provided, $_, does not match [ NTWXY]" }
240             ;
241              
242             =item NAB::Type::Str
243              
244             A Str that is restricted to the BECS EBCDIC character set
245              
246             =cut
247              
248             my $EBCDIC_re = qr/[^A-Za-z0-9^_[\]'\'',?;:=#\/.*()&%!$ \@+-]/a;
249              
250             subtype 'NAB::Type::Str'
251             => as 'Maybe[Str]',
252             => where {
253             !defined( $_ )
254              
255             # check for anything outside the BECS EBCDIC char set
256             or $_ !~ $EBCDIC_re;
257             }
258             => message { "Str provided $_ contains non BECS EBCDIC chars" }
259             ;
260              
261             =back
262              
263             =head1 METHODS
264              
265             =head4 add_max_string_attribute
266              
267             Helper method to allow easier definition of NAB::Type::Str types that
268             are limited to a particular lengths. For example:
269              
270             __PACKAGE__->add_max_string_attribute(
271             'RecipientNumber[20]'
272             is => 'ro',
273             required => 0,
274             );
275              
276             Is equivalent to:
277              
278             subtype 'NAB::Type::RecipientNumber'
279             => as 'Maybe[NAB::Type::Str]'
280             => where {
281             ! defined( $_ )
282             or length( $_ ) <= 20
283             }
284             => message {
285             "The string provided for recipient_number"
286             . " was outside 1..20 chars"
287             }
288             ;
289              
290             __PACKAGE__->meta->add_attribute( 'recipient_number',
291             isa => 'NAB::Type::RecipientNumber',
292             predicate => "_has_recipient_number",
293             is => 'ro',
294             required => 0,
295             );
296              
297             If you provide a suffix a trigger will be created to honour the requirement.
298             For example:
299              
300             __PACKAGE__->add_max_string_attribute(
301             'reel_sequence_number[2:trim_leading_zeros]',
302             ...
303             );
304              
305             Will make the trigger remove leading zeros whenever the attribute is set or
306             updated.
307              
308             Current supported suffixes are:
309              
310             * trim_leading_zeros
311              
312             =cut
313              
314             sub add_max_string_attribute (
315 177         449 $package,
316 177         382 $name_spec,
317 177         697 %attr_spec,
318 177     177 1 537 ) {
  177         409  
319 177         1513 my ( $subtype_name, $max_length, $trim )
320             = ( $name_spec =~ /^(\w+)\[(\d+)(:[A-z-]+)?\]$/ );
321              
322 177   66     946 $subtype_name //= $name_spec;
323 177         632 my $attr_name = decamelize( $subtype_name );
324              
325             subtype "NAB::Type::$subtype_name"
326             => as 'NAB::Type::Str'
327             => where {
328 876 100 100 876   6384 $max_length
329             ? ( !defined( $_ ) or length( $_ ) <= $max_length )
330             : 1;
331             }
332             => message {
333 29 100   29   1818 $_ =~ $EBCDIC_re
334             ? "Str provided $_ contains non BECS EBCDIC chars"
335             : "The string provided for $attr_name was outside 1..$max_length chars"
336             }
337 177         1911 ;
338              
339             $package->meta->add_attribute(
340             $attr_name,
341             isa => "NAB::Type::$subtype_name",
342             predicate => "_has_$attr_name",
343              
344             # trim via trigger if required
345             (
346             $trim
347             ? (
348             trigger => sub {
349 5     5   18 my ( $self, $value, $old_value ) = @_;
350              
351 5 50       42 $value =~ s/^0+// if $trim eq ':trim_leading_zeros';
352              
353 5         284 $self->{ $attr_name } = $value;
354              
355             } )
356 177 100       317377 : ()
357             ),
358              
359             %attr_spec,
360             );
361             }
362              
363             # taken from Mojo::Util - rather than pulling in the
364             # entire Mojolicious dist, we'll just "inline" it
365             sub decamelize {
366 274     274 0 676 my $str = shift;
367 274 100       1699 return $str if $str !~ /^[A-Z]/;
368              
369             # snake_case words
370             return join '-', map {
371 97         452 join( '_', map { lc } grep { length } split /([A-Z]{1}[^A-Z]*)/ )
  97         724  
  193         1041  
  386         811  
372             } split /::/, $str;
373             }
374              
375             1;