fix TeleAPI import (what kind of crack was Christopher smoking that he couldn't fix...
[freeside.git] / FS / FS / cdr / acmepacket.pm
1 package FS::cdr::acmepacket;
2
3 =head1 NAME
4
5 FS:cdr::acmepacket - CDR import definition based on Acme Packet Net-Net 4000
6
7 =head1 DESCRIPTION
8
9 The Acme Packet NetNet 4000 S-CX6.4.0 generates an odd cdr log format:
10
11  - Each row in the CSV may be in one of many various formats, some of
12    them undocumented.
13  - Columns are inconsistently enclosed with " characters
14  - Quoted column values may, themselves, contain unescaped quote marks.
15    This breaks Text::CSV (well technically the FORMAT is broken, not
16    Text::CSV).
17  - A single call can generate multiple CDR records.  The only records we're
18    interested in are billable calls:
19    - These are called "Stop Record CSV Placement" in Acme Packet documentation
20    - These will always contain a "2" as the first column value
21    - These rows may be 175 or 269 fields in length.  It's unclear if the
22      undocumented 269 column rows are an intentional Acme Packet format, or
23      a bug in their switch.  The extra columns are inserted at idx 115,
24      and can safely be disregarded.
25
26 NOTE ON DATE PARSING:
27
28   The Acme Packet manual doesn't describe it's date format in detail.  The sample
29   we were given contains only records from December.  Dates are formatted like
30   so: 15:54:56.868 PST DEC 18 2017
31
32   I gave my best guess how they will format the month text in the parser
33   FS::cdr::_cdr_date_parse().  If this format doesn't import records on a
34   particular month, check there.
35
36 =cut
37
38 use strict;
39 use warnings;
40 use vars qw(%info);
41 use base qw(FS::cdr);
42 use FS::cdr qw(_cdr_date_parse);
43 use Text::CSV;
44
45 my $DEBUG = 0;
46
47 my $cbcsv = Text::CSV->new({binary => 1})
48   or die "Error loading Text::CSV - ".Text::CSV->error_diag();
49
50 # Used to map source format into the contrived format created for import_fields
51 # $cdr_premap[ IMPORT_FIELDS_IDX ] = SOURCE_FORMAT_IDX
52 my @cdr_premap = (
53   9,  # clid
54   9,  # src
55   10, # dst
56   22, # channel
57   21, # dstchannel
58   26, # src_ip_addr
59   28, # dst_ip_addr
60   13, # startdate
61   14, # answerdate
62   15, # enddate
63   12, # duration
64   12, # billsec
65   3,  # userfield
66 );
67
68 our %info = (
69   name   => 'Acme Packet',
70   weight => 600,
71   header => 0,
72   type   => 'csv',
73
74   import_fields => [
75     # freeside      # [idx] acmepacket
76     'clid',         # 9  Calling-Station-Id
77     'src',          # 9  Calling-Station-Id
78     'dst',          # 10 Called-Station-Id
79     'channel',      # 22 Acme-Session-Ingress-Realm
80     'dstchannel',   # 23 Acme-Session-Egress-Realm
81     'src_ip_addr',  # 26 Acme-Flow-In-Src-Adr_FS1_f
82     'dst_ip_addr',  # 28 Acme-Flow-In-Dst-Addr_FS1_f
83     'startdate',    # 13 h323-setup-time
84     'answerdate',   # 14 h323-connect-time
85     'enddate',      # 15 h323-disconnect-time
86     'duration',     # 12 Acct-Session-Time
87     'billsec',      # 12 Acct-Session-Time
88     'userfield',    # 3 Acct-Session-Id
89   ],
90
91   row_callback => sub {
92     my $line = shift;
93
94     # Only process records whose first column contains a 2
95     return undef unless $line =~ /^2\,/;
96
97     # Replace unescaped quote characters within quote-enclosed text cols
98     $line =~ s/(?<!\,)\"(?!\,)/\'/g;
99
100     unless( $cbcsv->parse($line) ) {
101       warn "Text::CSV Error parsing Acme Packet CDR: ".$cbcsv->error_diag();
102       return undef;
103     }
104
105     my @row = $cbcsv->fields();
106     if (@row == 269) {
107       # Splice out the extra columns
108       @row = (@row[0..114], @row[209..@row-1]);
109     } elsif (@row != 175) {
110       warn "Unknown row format parsing Acme Packet CDR";
111       return undef;
112     }
113
114     my @out = map{ $row[$_] } @cdr_premap;
115
116     if ($DEBUG) {
117       warn "~~~~~~~~~~pre-processed~~~~~~~~~~~~~~~~ \n";
118       warn "$_ $out[$_] \n" for (0..@out-1);
119     }
120
121     # answerdate, enddate, startdate
122     for (7,8,9) {
123       $out[$_] = _cdr_date_parse($out[$_]);
124       if ($out[$_] =~ /\D/) {
125         warn "Unparsable date in Acme Packet CDR: ($out[$_])";
126         return undef;
127       }
128     }
129
130     # clid, dst, src CDR field formatted as one of the following:
131     #   'WIRELESS CALLER'<sip:12513001300@4.2.2.2:5060;user=phone>;tag=SDepng302
132     #   <sip:12513001300@4.2.2.2:5060;user=phone>;tag=SDepng302
133     #   '12513001300'<sip:4.2.2.2:5060;user=phone>;tag=SDepng302
134
135     # clid
136     $out[0] = $out[0] =~ /^\'(.+)\'/ ? $1 : "";
137
138     # src, dst
139     # Use the 7+ digit number as the src/dst phone number.
140     # Prefer using the number within <sip> label.  If there is not one,
141     # allow using number from caller-id text field.
142     for (1,2) {
143       my $f = $out[$_];
144       $out[$_] =~ s/^\'.+\'//g; # strip caller id label portion
145       if ($out[$_] =~ /(\d{7,})/) {
146         # Using number from <sip>
147         $out[$_] = $1;
148       } elsif ($f =~ /(\d{7,})/) {
149         # Using number from caller-id text
150         $out[$_] = $1;
151       } else {
152         # No phone number, perhaps an IP only call
153         $out[$_] = undef;
154       }
155     }
156
157     if ($DEBUG) {
158       warn "~~~~~~~~~~post-processed~~~~~~~~~~~~~~~~ \n";
159       warn "$_ $out[$_] \n" for (0..@out-1);
160     }
161
162     # I haven't seen commas in sample data text fields.  Extra caution,
163     # mangle commas into periods as we pass back to importer
164     join ',', map{ $_ =~ s/\,/\./g; $_ } @out;
165   },
166 );
167
168 1;