what was old is new again user/csharp/lp1902939_student_card_integration_part_deux
authorChris Sharp <csharp@georgialibraries.org>
Fri, 5 Nov 2021 18:53:08 +0000 (14:53 -0400)
committerChris Sharp <csharp@georgialibraries.org>
Fri, 5 Nov 2021 18:53:08 +0000 (14:53 -0400)
Signed-off-by: Chris Sharp <csharp@georgialibraries.org>
Open-ILS/src/support-scripts/import_student_data.pl

index 0b9b27d..ce0ea2d 100755 (executable)
 # This script is for importing student data from a CSV
 # exported from a student information system (SIS).
 #
-
-#TODO: STFP delete perms, create Exceptions dir
-
 use warnings;
 use strict;
 use Getopt::Long;
+#use Net::FTP;
+#use Net::SFTP::Foreign;
+#use IO::Socket::SSL;
 use Text::CSV qw/ csv /;
-use OpenILS::Utils::Cronscript;
-use OpenSRF::Utils::Logger qw/ $logger /;
+use DBI;
+use Try::Tiny;
 use Date::Parse;
-# something to do rsync/file transfer
 use File::Rsync;
 use Data::Dumper;
 
-# TODO: setup command line options to 
-# control things like remote server, etc.
-
-
-# initiate Cronscript
-my $cscript = OpenILS::Utils::Cronscript->new();
-# set up a cstore editor
-my $editor = $cscript->editor(xact=>1);
-# set up AppUtils
-my $U = 'OpenILS::Application::AppUtils';
-
-# set up variables
-my $work_dir_prefix = "/tmp";
-my $in_dir_prefix = "/home/opensrf/districts";
+# 0 = no stdout messages
+# 1 = basic
+# 2 = detailed
+my $debug = 1;
 my $prefix = $ARGV[1] if $ARGV[1];
 my @retrieved_files = $ARGV[0] if $ARGV[0];
-my $is_opt_out;
+my $remote_path_base = "/sftp";
+my $remote_file_dir = "Files";
+# TODO: develop a reliable query for getting the 
+# student card - or use YAOUS or something to set
+# the profile.
+my $profile = 61; # Student Card
+my $district_id = 0;
+my $error_message;
 my $imports;
 my $updates;
 my $deletes;
+my $opt_out;
 my @exceptions;
-my @required_fields = qw(
-    school_id
-    student_id
-    student_fname
-    student_lname
-    student_dob 
-    address_street1
-    address_city
-    address_state
-    address_postal
-    grade
+my @bad_student_ids;
+my $sth_district;
+
+# set up the DB connection
+my $dbh = DBI->connect
+(
+    "dbi:Pg:service=evergreen",
+    undef,
+    undef,
+    {
+        AutoCommit => 0,
+        RaiseError => 1,
+        PrintError => 0
+    }
+) or die DBI->errstr;
+
+# set up the SQL queries
+
+# get the districts
+my $district_sql = "select * from student_card.district where active = true";
+
+# get a specific district when in single-file mode
+my $single_district_sql = "select * from student_card.district where code = ?";
+
+# gather previously-processed filenames - populated below
+my $prev_files_sql;
+
+# check that user exists
+my $check_barcode_sql = "select 1 from actor.card where barcode = ?";
+
+# insert user
+my $insert_user_sql = qq(
+insert into actor.usr (
+    profile,
+    usrname,
+    email,
+    passwd,
+    ident_type,
+    ident_value,
+    ident_type2,
+    ident_value2,
+    first_given_name,
+    second_given_name,
+    family_name,
+    day_phone,
+    home_ou,
+    dob,
+    expire_date,
+    juvenile,
+    guardian,
+    name_keywords
+) values (
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    ?
+));
+
+my $school_data_sql = "select home_ou, name, addr_county from student_card.school where district_id = ? and state_id = ?";
+
+my $insert_address_sql = qq(
+insert into actor.usr_address (
+    usr,
+    street1,
+    street2,
+    city,
+    county,
+    state,
+    country,
+    post_code
+) values (
+    (select id from actor.usr where usrname = ?),
+    ?,
+    ?,
+    ?,
+    ?,
+    ?,
+    'USA',
+    ?
+));
+
+my $link_addr_sql = qq(
+update actor.usr
+set mailing_address = (
+    select id 
+    from actor.usr_address
+    where usr in (
+        select id
+        from actor.usr
+        where usrname = ?
+    )
+)
+where usrname = ?
 );
 
-# we start by getting the active school districts
-# via cstore
-sub get_active_districts {
-    my $districts = $editor->search_student_card_district(
-        {active => 't'});
-    return $districts;
-}
-
-
-sub get_files {
-    # for each active district, check the FTP server for files
-    # and copy them over 
-    my $district = shift;
-    my $code = $district->code;
-    my $prev_files = get_prev_files($district);
-    
-    # TODO: define the directory structure above
-    my $work_dir = "$work_dir_prefix/$code/";
-    mkdir $work_dir unless -d $work_dir;
-    my $in_dir = "$in_dir_prefix/$code/In/";
-    $logger->info("StudentCard: rsync-ing from $in_dir to $work_dir");
-    my $rsync = File::Rsync->new(
-        archive      => 1,
-        compress     => 1);
-    my $try = $rsync->exec(
-        src => $in_dir, 
-        dest => $work_dir,
-        exclude => $prev_files);
-    my $error = $!;
-    return $error unless $try;
-    my @new_files;
-    opendir(DIR, $work_dir) or die $!;
-    while (my $file = readdir(DIR)) {
-        next if (grep(/$file/, @$prev_files)) || ($file =~ m/^\./);
-        push(@new_files, "$work_dir/$file");
-        $logger->info("StudentCard: $file is a new file");
-    }
-    closedir(DIR);
-    my $new_files = \@new_files;
-    return $new_files;
-}
+my $insert_card_sql = qq(
+insert into actor.card (
+    usr,
+    barcode
+) values(
+    (select id from actor.usr where usrname = ?),
+    ?
+));
+
+my $link_card_sql = qq(
+update actor.usr
+set card = (
+    select id
+    from actor.card
+    where barcode = ?
+)
+where usrname = ?
+and not deleted
+);
 
-sub get_schools {
-    my $district = shift;
-    my $district_id = $district->id;
-    my $district_name = $district->name;
-    $logger->info("StudentCard: Getting schools for $district_name");
-    my $schools = $editor->search_student_card_school(
-       {district_id => $district_id}
-    );
-    return $schools;
-}
+my $insert_import_sql = qq(
+insert into student_card.import (
+    district_id,
+    filename,
+    error_message
+    ) values (
+    ?,
+    ?,
+    ?
+));
+
+# update user
+my $get_user_id_sql = qq (
+select usr
+from actor.card
+where barcode = ?
+);
 
-sub get_prev_files {
-    my $district = shift;
-    my $district_id = $district->id;
-    my $district_name = $district->name;
-    my $prev_files = $editor->search_student_card_import(
-        {district_id => $district_id}
-    );
-    my @prev_files;
-    for my $file (@$prev_files) {
-        my $filename = $file->filename;
-        push(@prev_files, $filename);
-    }
-    my $filelist = join(' ', @prev_files);
-    $logger->info("StudentCard: Previous files for $district_name: $filelist");
-    return \@prev_files;
-}
+my $get_addr_sql = qq(
+select mailing_address
+from actor.usr
+where id = ?
+);
 
-sub import_csv {
-    my $csv_file = shift;
-    $is_opt_out = 1 if $csv_file =~ /OptOut/;
-    my $student_entries = csv (
-        in => "$csv_file",
-        headers => "lc", #lc = "lowercase"
-        empty_is_undef => 1
-    ) or die Text::CSV->error_diag;
-    return $student_entries; # this is an array of hashes
-}
+my $update_user_sql = qq(
+update actor.usr set
+    first_given_name = ?,
+    second_given_name = ?,
+    family_name = ?,
+    dob = ?,
+    day_phone = ?,
+    guardian = ?,
+    name_keywords = ?,
+    ident_type = ?,
+    ident_value = ?,
+    ident_value2 = ?,
+    home_ou = ?,
+    expire_date = ?
+where id = ?
+);
 
-sub check_required_fields {
-    my $student = shift;
-    for my $field (@required_fields) {
-        unless ( $student->{$field} ) {
-            my $id = $student->{student_id};
-            $logger->info("StudentCard: no $field for student ID $id");
-            add_exception($student, "Missing $field");
-            return 0;
-        }
-    }
-    return 1;   
-}
+my $update_address_sql = qq(
+update actor.usr_address set
+    street1 = ?,
+    street2 = ?,
+    city = ?,
+    county = ?,
+    state = ?,
+    post_code = ?
+where id = ?
+);
 
-sub lookup_school {
-    my ($schools, $school_id) = @_;
-    for my $school (@$schools) {
-        my $state_id = $school->state_id;
-        if (int($school_id) == $state_id) {
-            return $school;
-        }
-    }
-    $logger->warn("StudentCard: No school entered for $school_id");
-    return 0;
-}
+# delete user
+my $delete_user_sql = qq(
+select actor.usr_delete(?, 0)
+);
 
 sub calculate_expire_date {
     my @now = localtime();
     my ($now_year, $now_month) = ($now[5] + 1900, $now[4] + 1);
     my $expire_year = $now_year;
-    if ($now_month > 6) { # any student registered before end of June gets expire date the following 10/15
+    if ($now_month > 6) { # any student registered before end of June gets expire date the following 9/15
         $expire_year += 1;
     }
     my $expire_date = $expire_year . "-10-15";
@@ -186,236 +250,377 @@ sub calculate_expire_date {
 
 sub calculate_password {
     my $dob = shift;
-    return 0 unless $dob;
-    my ($ss,$mm,$hh,$day,$month,$year,$zone) = strptime($dob);
-    if ($year =~ /(\d{3})/) {
-        $year += 1900;
-    } else{
-        $year += 2000;
-    }
-    $month += 1;
-    if ($month < 10) {
-            $month = "0"."$month";
-    }
-    my $password = $month . $year;
+    my @elements = split('-', $dob);
+    my ($dob_month, $dob_year) = ($elements[1], $elements[0]);
+    #if ($dob_month < 10) {
+    #    $dob_month = "0" . $dob_month;
+    #}
+    my $password = $dob_month . $dob_year;
     return $password;
 }
 
-sub get_name_keywords {
-    my $grade = shift;
-    my $name_keywords;
-    if ($grade && $grade =~ /^\d+$/) {
-        if ($grade == 0) {
-            $name_keywords = "GradeK";
-        } elsif ($grade < 0) {
-            $name_keywords = "GradePK";
-        } elsif ($grade < 10) {
-            $name_keywords = "Grade0" . $grade;
-        } else {
-            $name_keywords = "Grade" . $grade;
-        }
-    } elsif ($grade) {
-        $name_keywords = $grade;
-    }
-    return $name_keywords;
-}
-
-# copied from Ecard.pm...
-# Create actor.usr perl object and populate column data
-sub make_user {
-    my ($student, $school, $district_code) = @_;
-
-    my $au = Fieldmapper::actor::user->new;
-
-    print Dumper($au);
-
-    # set up variables
-    my $student_id      = $student->{student_id};
-    my $grade           = $student->{grade};
-    my $provided_pass   = $student->{student_password};
-    my $dob             = $student->{student_dob};
-
-    # populate fields 
-    $au->isnew(1);
-    $au->first_given_name($student->{student_fname});
-    $au->second_given_name($student->{student_mname});
-    $au->family_name($student->{student_lname});
-    $au->dob($dob);
-    $au->day_phone($student->{student_phone});
-    $au->email($student->{student_email});
-    $au->passwd($provided_pass ? $provided_pass : calculate_password($dob));
-    $au->guardian($student->{parent_guardian} ? $student->{parent_guardian} : $school->name);
-    $au->ident_type(3); # Other
-    $au->ident_value($student_id);
-    $au->expire_date(calculate_expire_date);
-    $au->juvenile(1);
-    $au->name_keywords(get_name_keywords($grade));
-    $au->net_access_level(1); # Filtered
-    $au->profile($school->eg_perm_group);
-    $au->home_ou($school->home_ou);
-    $au->usrname($district_code.$student_id);
-
-    return $au;
-
-}
-
-sub add_addresses {
-    my ($student, $user) = @_;
-
-    # address fields
-
-    my $physical_addr = Fieldmapper::actor::user_address->new;
-    $physical_addr->isnew(1);
-    $physical_addr->usr($user->id);
-    $physical_addr->address_type('PHYSICAL');
-    $physical_addr->within_city_limits('f');
-
-    my $mailing_addr = Fieldmapper::actor::user_address->new;
-    $mailing_addr->isnew(1);
-    $mailing_addr->usr($user->id);
-    $mailing_addr->address_type('MAILING');
-    $mailing_addr->within_city_limits('f');
-
-    for my $type ($mailing_addr, $physical_addr) {
-        $type->street1($student->{address_street1});
-        $type->street2($student->{address_street2});
-        $type->city($student->{address_city});
-        $type->county($student->{address_county});
-        $type->state($student->{address_state});
-        $type->post_code($student->{address_postal});
-        $type->country('USA');
-    }
-
-    # Use as both billing and mailing via virtual ID.
-    $physical_addr->id(-1);
-    $mailing_addr->id(-1);
-    $user->billing_address(-1);
-    $user->mailing_address(-1);
-
-    $user->billing_address($physical_addr);
-    $user->mailing_address($mailing_addr);
-    $user->addresses([$physical_addr, $mailing_addr]);
-
-    return 1;
-}
-
-
-sub add_card {
-    my ($student, $user, $prefix) = @_;
-    my $id = $student->{student_id};
-    my $barcode = "$prefix"."$id";
-
-    my $card = Fieldmapper::actor::card->new;
-    $card->id(-1);
-    $card->isnew(1);
-    $card->usr($user->id);
-    $card->barcode($barcode);
-
-    # username defaults to barcode
-    $user->usrname($barcode);
-    $user->card($card);
-    $user->cards([$card]);
-
-    return 1;
-}
-
-
-sub do_login {
-    my $auth = $U->simplereq(
-    'open-ils.auth_internal',
-    'open-ils.auth_internal.session.create',
-    {user_id => 1, org_unit => 394, login_type => 'temp'}
-    );
-    
-    return unless $auth && $auth->{textcode} eq 'SUCCESS';
-
-    my $authtoken = $auth->{payload}->{authtoken};
+sub get_files {
+    my $district_code = shift;
     
-    return $authtoken;
 }
 
-
-sub user_exists {
-    my ($student) = @_;
-    my $lname = $student->{lname};
-    my $ident = $student->{student_id};
-    my $user = $editor->search_actor_user({
-        family_name => $lname,
-        ident_value => $ident
-    });
-    return $user if $user;
-    return 0;
-
-}
-
-
-sub save_user {
-    my ($user, $authtoken) = @_;
-
-    print Dumper($user);
-    print $authtoken;
+if (@retrieved_files) {
+    # we're in single-file mode, so get the district ID for later use
+    $sth_district = $dbh->prepare($single_district_sql);
+    $sth_district->execute($prefix);
+    while (my $district = $sth_district->fetchrow_hashref) {
+        $district_id = $district->{id};
+    }
+    die "No district with code $prefix." unless $district_id;
+    $sth_district->finish();
+} else {
+    # go out and get the files we need
+    $sth_district = $dbh->prepare($district_sql);
+    $sth_district->execute();
+    while (my $district = $sth_district->fetchrow_hashref) {
+        # check incoming FTP directory for the district
+        $district_id = $district->{id};
+        $prev_files_sql = "select filename from student_card.import where district_id = $district_id";
+        $prefix = $district->{code};
+        my $local_dir = "/tmp";
+        my $remote_dir = "$remote_path_base/$prefix/$remote_file_dir"
+        my @remote_files;
+        my $rsync = File::Rsync->new(
+            archive      => 1,
+            compress     => 1); 
+        if ($debug) {
+            print "Now processing files for " . $district->{code} . "\n";
+        }
+        if ($debug == 2) {
+            print "District Prefix: $prefix\n";
+            print "Local Directory: $local_dir\n";
+        }
+        
+                        
     
-    my $save = $U->simplereq(
-        'open-ils.actor',
-        'open-ils.actor.patron.update',
-        $authtoken, $user
-    );
-
-    if ($U->is_event($save)) {
-        my $msg = "Unable to save account: " . $save->{textcode};
-        $logger->error("StudentCard: $msg");
-        return 0;
+        if ($debug == 2) {
+            print "Remote File Listing: @remote_files\n";
+        }
+        ## walk through the remote files and filter out any that do
+        ## not meet our naming requirements
+        foreach my $file (@remote_files) {
+            # XXX hard-coding the "OptOut" string here - maybe make it a setting? 
+            if ($file =~ /${prefix}(OptOut)?_\d{12}/) { 
+                my $previous = $dbh->selectcol_arrayref($prev_files_sql);
+                # skip files we have already retreived
+                unless (grep /$file/, @$previous) {
+                    if ($debug) {
+                        print "Getting $file\n";
+                    }
+                    if ($ftp_protocol =~ '^ftp(es)?') {
+                        $ftp->get($file, "$local_dir/$file")
+                        or die "Could not retrieve $file: ", $ftp->message;
+                        push (@retrieved_files, "$local_dir/$file");
+                    } elsif ($ftp_protocol = 'sftp') {
+                        $sftp->get($file, "$local_dir/$file")
+                        or die "Could not retrieve $file: " . $sftp->error;
+                        push (@retrieved_files, "$local_dir/$file");
+                    }
+                } else {
+                    if ($debug) {
+                        print "$file already retreived\n";
+                    }
+                }
+            }
+        }
+        if ($ftp_protocol =~ '^ftp(es)?') {
+            $ftp->quit;
+        } elsif ($ftp_protocol = 'sftp') {
+            $sftp->disconnect;
+        }
     }
-    return 1;
-}
-
-
-sub add_exception {
-    my ($student, $error) = @_;
-    $student->{error} = $error;
-    push (@exceptions, $student);
+    $sth_district->finish;
 }
 
-# We've built everything, now get to work!
 
-my $districts = get_active_districts;
-my $authtoken = do_login;
-die "No authtoken available, cannot continue\n" unless $authtoken;
-
-# for each active district,
-for my $district(@$districts) {
-    my $schools = get_schools($district);
-    my $district_name = $district->name;
-    my $district_code = $district->code;
-    
-    my $retrieved_files = get_files($district);
-
-    if (scalar(@$retrieved_files) > 0) {
-        $logger->info("StudentCard: Copying files from $district_name");
-    } else {
-        $logger->info("StudentCard: No new files for $district_name");
-        next;
-    }
+foreach my $csv_file (@retrieved_files) {
+    $imports = 0;
+    $updates = 0;
+    $deletes = 0;
+    $opt_out = 1 if $csv_file =~ /OptOut/;
+    # a couple of options here - we can assume that the headers are correct
+    # and use headers => "auto" here, which will fail to work if there's a
+    # typo in the header names, or we can do headers => "skip" and trust
+    # that the export follows our prescribed order.
+    # For now, we'll trust correct order over correct header names.
+    my $student_entries = csv (in => "$csv_file", headers => "skip", empty_is_undef => 1)
+        or die Text::CSV->error_diag;
+    foreach my $student (@$student_entries) {
+        # set up variables
+        my $school_id       = @$student[0]; 
+        my $student_id      = @$student[1]; 
+        my $first_name      = @$student[2]; 
+        my $middle_name     = @$student[3]; 
+        my $last_name       = @$student[4]; 
+        my $dob             = @$student[5]; 
+        my $phone           = @$student[6]; 
+        my $email           = @$student[7]; 
+        my $street1         = @$student[8]; 
+        my $street2         = @$student[9]; 
+        my $city            = @$student[10]; 
+        my $county          = @$student[11];
+        my $state           = @$student[12]; 
+        my $post_code       = @$student[13]; 
+        my $parent_guardian = @$student[14]; 
+        my $grade           = @$student[15];
+        my $barcode         = $prefix . $student_id;
+        my $username        = $barcode;
+        my $password        = calculate_password($dob);         
+        my $ident_type      = 3; # Other
+        my $ident_value     = $student_id;
+        my $expire_date     = calculate_expire_date;
+        my $juvenile        = "true";
+        my $name_keywords;
+        if ($grade && $grade =~ /^\d+$/) {
+            if ($grade == 0) {
+                $name_keywords = "GradeK";
+            } elsif ($grade < 0) {
+                $name_keywords = "GradePK";
+            } elsif ($grade < 10) {
+                $name_keywords = "Grade0" . $grade;
+            } else {
+                $name_keywords = "Grade" . $grade;
+            }
+        } elsif ($grade) {
+            $name_keywords = $grade;
+        }
+        # figure out how to get the home ou from the DB based
+        # on the school ID
+        my $sth_school_data = $dbh->prepare($school_data_sql);
+        $sth_school_data->execute($district_id, $school_id);
+        my @school_result = $sth_school_data->fetchrow_array;
+        $sth_school_data->finish;
+        my $home_branch = $school_result[0];
+        my $school_name = $school_result[1];
+        my $school_county = $school_result[2];
+        my $ident2_value = $school_name;
+        # make sure we have a usable county entry
+        # if not, add from the school data
+        if (!$county || $county =~ m/\d/) {
+            $county = $school_county;
+        }
+        # check that we have the required data for the student
+        if (!$parent_guardian) {
+            $parent_guardian = $school_name
+        }
+        #TODO: write out an exceptions file
+        my $no_import = 0;
+        unless (
+            $school_id && 
+            $student_id &&
+            $first_name &&
+            $last_name &&
+            $dob &&
+            $street1 &&
+            $city &&
+            $state &&
+            $post_code &&
+            $home_branch ) {
+            if ($debug == 2) {
+                print "Student $student_id is an exception.\n";
+            }
+            push @exceptions, $student;
+            # push IDs into array for email
+            push @bad_student_ids, $student_id;
+            $no_import = 1;
+        }
+        unless ($no_import) {
+            # check if we already have the student account
+            my $sth_check_barcode = $dbh->prepare($check_barcode_sql);
+            $sth_check_barcode->execute($barcode);
+            my $already_imported = $sth_check_barcode->fetchrow_array;
+            $sth_check_barcode->finish;
+            my $sth_user_id = $dbh->prepare($get_user_id_sql);
+            $sth_user_id->execute($barcode);
+            my @user_result = $sth_user_id->fetchrow_array;
+            $sth_user_id->finish;
+            my $user_id = $user_result[0];
+            if ($already_imported && !$opt_out) {
+                my $sth_addr = $dbh->prepare($get_addr_sql);
+                $sth_addr->execute($user_id);
+                my @addr_result = $sth_addr->fetchrow_array;
+                $sth_addr->finish;
+                my $addr_id = $addr_result[0];
+                my @update_user_data = (
+                    $first_name,
+                    $middle_name,
+                    $last_name,
+                    $dob,
+                    $phone,
+                    $parent_guardian,
+                    $name_keywords,
+                    $ident_type,
+                    $ident_value,
+                    $ident2_value,
+                    $home_branch,
+                    $expire_date,
+                    $user_id
+                );
+                my @update_addr_data = (
+                    $street1,
+                    $street2,
+                    $city,
+                    $county,
+                    $state,
+                    $post_code,
+                    $addr_id
+                );
+                # do the update
+                if ($debug) {
+                    print "Updating $barcode\n";
+                }
+                try {
+                    # update user
+                    if ($debug == 2) {
+                        print "Update user data: @update_user_data\n";
+                    }
+                    my $sth_update_user = $dbh->prepare($update_user_sql);
+                    $sth_update_user->execute(@update_user_data);
+                    # update address
+                    if ($debug == 2) {
+                        print "Update address data: @update_addr_data\n";
+                    }
+                    my $sth_update_addr = $dbh->prepare($update_address_sql);
+                    $sth_update_addr->execute(@update_addr_data);
+                    $dbh->commit;
+                    $sth_update_user->finish;
+                    $sth_update_addr->finish;
+                    $updates++;
+                } catch {
+                    warn "Transaction aborted because $_";
+                    eval { $dbh->rollback };
+                };
+            } elsif ($already_imported && $opt_out ) {
+                if ($debug) {
+                    print "Deleting $barcode\n";
+                }
+                try {
+                    # delete user
+                    my $sth_delete_user = $dbh->prepare($delete_user_sql);
+                    $sth_delete_user->execute($user_id);
+                    $dbh->commit;
+                    $sth_delete_user->finish;
+                    $deletes++
+                } catch {
+                    warn "Transaction aborted because $_";
+                    eval { $dbh->rollback };
+                };
+            } else {
+                # import user
+                my @user_data = (
+                    $profile, 
+                    $username, 
+                    $email,
+                    $password,
+                    $ident_type,
+                    $ident_value,
+                    $ident_type,
+                    $ident2_value,
+                    $first_name,
+                    $middle_name,
+                    $last_name,
+                    $phone,
+                    $home_branch,
+                    $dob,
+                    $expire_date,   
+                    $juvenile,
+                    $parent_guardian,
+                    $name_keywords
+                );
+                my @addr_data = (
+                    $username,
+                    $street1,
+                    $street2,
+                    $city,
+                    $county,
+                    $state,
+                    $post_code,
+                );
+                my @card_data = (
+                    $username,
+                    $barcode
+                );
+                my @link_card_data = (
+                    $barcode,
+                    $username
+                );
+                my @link_addr_data = (
+                    $username,
+                    $username
+                );
+                    
+               # do the import here
+                if ($debug) {
+                    print "Importing $barcode\n";
+                    }
+                    try {
+                        # insert user
+                        if ($debug == 2) {
+                            print "Inserting user data: @user_data\n";
+                        }
+                        my $sth_insert_user = $dbh->prepare($insert_user_sql);
+                        $sth_insert_user->execute(@user_data);
+                        # insert address
+                        if ($debug == 2) {
+                            print "Inserting address data: @addr_data\n";
+                        }
+                        my $sth_insert_addr = $dbh->prepare($insert_address_sql);
+                        $sth_insert_addr->execute(@addr_data);
+                        # link address back to the user
+                        my $sth_link_addr = $dbh->prepare($link_addr_sql);
+                        $sth_link_addr->execute(@link_addr_data);
+                        # insert card
+                        if ($debug == 2) {
+                            print "Inserting card data: @card_data\n";
+                        }
+                        my $sth_insert_card = $dbh->prepare($insert_card_sql);
+                        $sth_insert_card->execute(@card_data);
+                        # link card back to the user
+                        my $sth_link_card = $dbh->prepare($link_card_sql);
+                        $sth_link_card->execute(@link_card_data);
+                        $dbh->commit;
+                        $sth_insert_user->finish;
+                        $sth_insert_addr->finish;
+                        $sth_link_addr->finish;
+                        $sth_insert_card->finish;
+                        $sth_link_card->finish;
+                    } catch {
+                        warn "Transaction aborted because $_";
+                        eval { $dbh->rollback };
+                    };
+                    $imports++;
+                }
+            
+            }
+        }
 
-    # for each retrieved file for the current district,
-    for my $csv_file (@$retrieved_files) {
+    # record the import in the DB
+    my @file_path = split('/', $csv_file);
+    my $filename = $file_path[-1]; # bare filename
+    try {     
+        my $sth_insert_import = $dbh->prepare($insert_import_sql);
+        #TODO: error handling that gets pushed to the DB
+        $sth_insert_import->execute($district_id, $filename, $error_message);
+        $dbh->commit;
+        $sth_insert_import->finish;
+    } catch {
+        warn "Transaction aborted because $_";
+        eval { $dbh->rollback };
+    };
+    # write exceptions to a file
+    my $exceptions_file = "/tmp/$filename.exceptions";
+    my $exceptions_ref = \@exceptions;
+    csv (in => $exceptions_ref, out => "$exceptions_file")
+        or die Text::CSV->error_diag;
+    print "\n\nImport statistics:\n\n\tImports: $imports\n\tUpdates: $updates\n\tOpt-Outs: $deletes\n\tExceptions: " . scalar(@exceptions) . "\n\n\tCreated exceptions file $exceptions_file.\n";
+    shift @retrieved_files;
+}
+# clean up
+$dbh->disconnect();
 
-        #import the file
-        $logger->info("Importing $csv_file for $district_name");
-        my $student_entries = import_csv($csv_file);
-    
-        #for each student row in the current file,
-        for my $student (@$student_entries) {
-            next unless check_required_fields($student);
-            #my $existing_user = user_exists($student, $district_code); 
-            #$logger->info("CSHARP: \$existing_user = " . Dumper($existing_user));
-            my $school = lookup_school($schools, $student->{school_id});
-            add_exception($student, "No school found with State ID $student->{school_id}") && next unless $school;
-            my $user = make_user($student, $school, $district_code);
-            add_addresses($student, $user);
-            save_user($user, $authtoken);
-            add_card($student, $user, $district_code);
-            save_user($user, $authtoken); # not sure this is right...
-        } # student
-    } # csv_file
-} # district