#!/usr/bin/perl # Copyright © 2018, Bradley M. Kuhn # License: AGPL-3.0-or-later use strict; use warnings; use autodie qw(:all); use Getopt::Long; use File::Spec; use Date::Manip qw(ParseDate UnixDate); our $RT_CMD; require 'rt-helper.pl'; my($PAYMENT_DIR, $VERBOSE, $INTERACTIVE, $PAYMENT_NUMBER, $INVOICE_LINE, $INTERN_SUCCESS_FILE, $INTERN_FAIL_FILE, $LEDGER_ENTRY_DATE, $SVN_CMD, $ROUND, $TRAVEL_NOTICE_TICKET); ############################################################################### sub LedgerTagFromTicket($$) { my($ticketSpec, $tag) = @_; open(my $rtPayFH, "-|", "$RT_CMD", "show", "-f", 'CF.{ledger-tags}', $ticketSpec); my $tagValue; while (my $tagsLine = <$rtPayFH>) { print STDERR "Tag Lookup for \"$ticketSpec\" for \"$tag\", searching this line for it: $tagsLine" if ($VERBOSE > 6); chomp $tagsLine; if ($tagsLine =~ /^(?:\s*|\s*CF[^:]+\s*}\s*:\s*);?$tag\s*:\s*(.+)\s*$/i) { $tagValue = $1; last; } } close $rtPayFH; return $tagValue; } ############################################################################### sub AllFormattedLedgerTagFromTicket($) { my($ticketSpec) = @_; my @tags; open(my $rtLedgerTagsFH, "-|", "$RT_CMD", "show", "-f", 'CF.{ledger-tags}', $ticketSpec); my $tagValue; my $start = 0; while (my $tagsLine = <$rtLedgerTagsFH>) { $start = 1 if $tagsLine =~ s/^\s*CF.{ledger-tags}\s*:\s+//; next unless $start; $tagsLine =~ s/^\s*//; push(@tags, " $tagsLine"); } close $rtLedgerTagsFH; return @tags; } ############################################################################### sub FindTaxTicketFromList(@) { my $taxTicket; foreach my $ticket (@_) { open(my $rtQueueFH, "-|", "$RT_CMD", "show", "-f", 'Queue', $ticket); while (my $queueLine = <$rtQueueFH>) { if ($queueLine =~ /\s*Queue\s*:\s*(\S+)\s*$/) { my $queue = $1; $taxTicket = $ticket if $queue =~ /accounts-taxinfo/; last; } } close $rtQueueFH; last if defined $taxTicket; } return $taxTicket; } ############################################################################### GetOptions("paymentDir=s" => \$PAYMENT_DIR, "verbose=i" => \$VERBOSE, "interactive" => \$INTERACTIVE, "paymentNumber=i" => \$PAYMENT_NUMBER, "rtCommand=s" => \$RT_CMD, "invoiceLine=s" => \$INVOICE_LINE, "internSuccessFile=s", \$INTERN_SUCCESS_FILE, "internFailFile=s", \$INTERN_FAIL_FILE, 'ledgerEntryDate=s' => \$LEDGER_ENTRY_DATE, "svnCommand=s" => \$SVN_CMD, "round=s" => \$ROUND, 'travelNoticeTicket=i' => \$TRAVEL_NOTICE_TICKET); $RT_CMD = '/usr/bin/rt' unless defined $RT_CMD; $SVN_CMD = '/usr/bin/svn' unless defined $SVN_CMD; $INTERACTIVE = 0 if not defined $INTERACTIVE; unless (defined $TRAVEL_NOTICE_TICKET) { print STDERR "usage: $0 --travelNoticeTicket= option is required and must be an integer\n"; exit 1; } unless (defined $ROUND and $ROUND =~ /^[\d\-]+$/) { print STDERR "usage: $0 --round= option is required and must formated as YYYY-MM\n"; exit 1; } unless (defined $LEDGER_ENTRY_DATE and $LEDGER_ENTRY_DATE =~ /^[\d\-]+$/) { print STDERR "usage: $0 --ledgerEntryDate= option is required and must be in ISO 8601 format\n"; exit 1; } unless (defined $INVOICE_LINE and $INVOICE_LINE =~ /^rt.*/) { print STDERR "usage: $0 --invoiceLine= option is required and must match an RT spec\n"; exit 1; } unless (defined $PAYMENT_DIR and -d $PAYMENT_DIR) { print STDERR "usage: $0 --paymentDir= option is required and directory must exist\n"; exit 1; } unless (-r $INTERN_SUCCESS_FILE and -f $INTERN_SUCCESS_FILE) { print STDERR "usage: $0 --internUpdateFile= option is required and must be readible text file\n"; exit 1; } unless (-r $INTERN_FAIL_FILE and -f $INTERN_FAIL_FILE) { print STDERR "usage: $0 --internFailFile= option is required and must be readible text file\n"; exit 1; } my %internCorrespond = ('success' => [], 'failed' => [] ); open (my $internUpdateFH, "<", File::Spec->catfile($PAYMENT_DIR, $INTERN_SUCCESS_FILE)); while (my $line = <$internUpdateFH>) { push(@{$internCorrespond{success}}, $line); } my @internFailData; open (my $internFailFH, "<", File::Spec->catfile($PAYMENT_DIR, $INTERN_FAIL_FILE)); while (my $line = <$internFailFH>) { push(@{$internCorrespond{failed}}, $line); } unless (defined $PAYMENT_NUMBER and $PAYMENT_NUMBER =~ /^[123]$/) { print STDERR "usage: $0 --paymentNumber= option is required and must be 1, 2 or 3\n"; exit 1; } $VERBOSE = 0 unless defined $VERBOSE; opendir(my $dh, $PAYMENT_DIR); my $oldInterns = 0; while (my $file = readdir $dh) { unless ($file =~ /^\s*(success|faile?d?)-(\S+)\.txt\s*$/i) { print STDERR "Skipping $file which does not match proper format...\n" if ($VERBOSE >= 2); next; } my($pass, $name) = ($1, $2); $pass = ($pass =~ /success/) ? 1 : 0; open(my $fh, "<", File::Spec->catfile($PAYMENT_DIR, $file)); my $mentorDate; while (my $line = <$fh> ) { if ($line =~ /^\s*Date\s*:\s*(.+)\s*$/) { $mentorDate = UnixDate(ParseDate($1), "%Y-%m-%d"); next; } } if (not defined $mentorDate) { print STDERR "\"$file\": Skipping: Inside that file there is no valid Date: header" ; next; } my(@nameComponents) = split(/\s*-\s*/, $name); my(@searchTerms); foreach my $name (@nameComponents) { push(@searchTerms, 'Subject LIKE "' . $name . '"'); } # Find the ticket number for this intern. my $ticket = Outreachy_FindUniqueTicket($ROUND, @searchTerms); if (not defined $ticket) { foreach my $term (@searchTerms) { print STDERR "\"$file\": full search failed, trying \"$term\" by itself\n" if ($VERBOSE > 6); $ticket = Outreachy_FindUniqueTicket($ROUND, ($term)); last if (defined $ticket); } } if (not defined $ticket) { if (not $INTERACTIVE) { print STDERR "\"$file\": TICKET-NOT-FOUND: Skipped: unable to to find a matching ticket.\n"; next; } else { # FIXME: prompt for ticket die "interactive mode not yet supported"; } } my $completedInternshipField = GetCustomFieldForTicket($ticket, "completed-internship"); if (not defined $completedInternshipField) { print STDERR "\"$file\": \"$ticket\": FIELD-NOT-FOUND: Skipping: cannot determine Entity from ticket.\n" ; next; } elsif ($completedInternshipField eq 'successful') { # Don't print to STDERR here, just keep a count since these are "old interns" $oldInterns++; next; } my $entity = LedgerTagFromTicket($ticket, 'Entity'); if (not defined $entity) { print STDERR "\"$file\": \"$ticket\": ENTITY-NOT-FOUND: Skipping: cannot determine Entity from ticket.\n" ; next; } if ($PAYMENT_NUMBER <= 1) { print STDERR "Sorry, script does not yet support first payment\n"; exit 1; } # Check to see if this payment was already made my $thisPayDate = PaymentDateByTicket($ticket, $PAYMENT_NUMBER); if (defined $thisPayDate) { print STDERR "\"$file\": \"$ticket\": Skipped: payment $PAYMENT_NUMBER", " was already made on \"$thisPayDate\""; if ($pass) { print STDERR ".\n"; } else { print STDERR "... BIG PROBLEM: the intern actually failed but got this payment.\n"; } next; } # Check to see if previous payment was sent payment my $prevPayNum = $PAYMENT_NUMBER - 1; my $lastPayDate = PaymentDateByTicket($ticket, $prevPayNum); if (not defined $lastPayDate) { print STDERR "\"$file\": \"$ticket\": Skipped: payment $prevPayNum was not made yet"; if ($pass) { print STDERR ".\n"; } else { print STDERR "... NOTE: previous payment was not sent; should it be sent now?\n"; } next; } my $expectVal = 'payment-' . $PAYMENT_NUMBER . "-approved"; if ($completedInternshipField eq $expectVal) { print STDERR "\"$file\": \"$ticket\": $PAYMENT_NUMBER PAYMENT-DONE: Skipped: completed-internship is ", "\"$completedInternshipField\" which indicates this payment round is in process.\n"; next; } $expectVal = 'payment-' . $prevPayNum . "-approved"; if ($completedInternshipField ne $expectVal) { print STDERR "\"$file\": \"$ticket\": Skipped: completed-internship field was ", "\"$completedInternshipField\" instead of \"$expectVal\".\n"; next; } my(%links) = GetLinksForTicket($ticket); if ($VERBOSE > 5) { use Data::Dumper; print STDERR "\"$file\": \"$ticket\": Found the following links: " , Data::Dumper->Dump([\%links]), "\n"; } my $taxTicket = FindTaxTicketFromList(@{$links{DependsOn}}); if (not defined $taxTicket) { print STDERR "\"$file\": \"$ticket\": Skipped: no tax ticket found.\n"; next; } my $reimbursementTicket = FindReimbursementTicketFromList($ROUND, @{$links{Members}}); if (not defined $reimbursementTicket) { print STDERR "\"$file\": \"$ticket\": Skipped: no reimbursement ticket found.\n"; next; } print STDERR "\"$file\": \"$ticket\": found a tax ticket of \"$taxTicket\"\n" if ($VERBOSE > 5); my $taxTicketStatus = GetStatusFromTicket($taxTicket); if ($taxTicketStatus ne "resolved") { print STDERR "\"$file\": \"$ticket\": TAX-TICKET-PENDING: \"$taxTicket\": Skipped: ", "tax ticket is in status \"$taxTicketStatus\" instead of \"resolved\"\n"; next; } my $mainTicketStatus = GetStatusFromTicket($ticket); if ($mainTicketStatus ne "needs-project-ok") { print STDERR "\"$file\": \"$ticket\": PREV-PAYMENT-INCOMPLETE: Skipped: ", "ticket is in status \"$mainTicketStatus\" instead of \"needs-project-ok\"\n"; next; } my $ticketNum = $ticket; $ticketNum =~ s%^.*ticket/(\d+).*$%$1%; my %paymentVals; if ($PAYMENT_NUMBER >= 2) { open(my $logFH, "-|", $RT_CMD, "show", $ticketNum); while (my $line = <$logFH>) { # Note that this will take the last one used, since rt log gives ticket traffic IN ORDER. if ($line =~ /^\s*([^:]+)\s*:(\s*.+)$/) { my($key, $val) = ($1, $2); $paymentVals{$key} = $val if $key =~ /(CONTRACTED NAME|PAYMENT NAME|PAYMENT METHOD)/i; print STDERR "\"$file\": \"$ticket\": rt show $ticketNum line match: $key $val for $line" if ($VERBOSE > 7); } } close $logFH; } print STDERR "\"$file\": \"$ticket\": processing to payment $PAYMENT_NUMBER state... "; my $successString = ($pass) ? "success" : "failed"; my $repositoryFile = File::Spec->catfile($PAYMENT_DIR, $mentorDate . "_" . $entity . '_' . $successString . '-report.mbox'); my $approvalTag = $repositoryFile; $approvalTag =~ s%^.*(Projects/Outreachy/.*)$%$1%; $approvalTag = " ;Approval: $approvalTag"; rename(File::Spec->catfile($PAYMENT_DIR, $file), $repositoryFile); system($SVN_CMD, "add", $repositoryFile); open(my $rtCorrespondFH, "|-", $RT_CMD, 'correspond', $ticket, '-m', '-'); my @dd; foreach my $line (@{$internCorrespond{$successString}}) { $line =~ s/FIXME_PAYMENT_NUMBER/$PAYMENT_NUMBER/g; $line =~ s/FIXME_INVOICE_DATE/$LEDGER_ENTRY_DATE/g; $line =~ s/FIXME_MENTOR_DATE/$mentorDate/g; push(@dd, $line); } print $rtCorrespondFH @dd; close $rtCorrespondFH; my $invoiceTicket = $INVOICE_LINE; $invoiceTicket =~ s%^.*ticket/(\d+).*$%$1%; system($RT_CMD, 'link', $invoiceTicket, 'refersto', $ticketNum); if ($pass) { open(my $rtCommentFH, "|-", $RT_CMD, 'comment', $ticket, '-m', '-'); print $rtCommentFH " ;Invoice: $INVOICE_LINE\n"; close $rtCommentFH; system($RT_CMD, "edit", $ticket, 'set', 'CF.{completed-internship}=payment-' . $PAYMENT_NUMBER . '-approved', 'Status=open'); my($leftA, $rightA); if ($PAYMENT_NUMBER == 1) { $leftA = ' $-500.00'; $rightA = ' $500.00'; } elsif ($PAYMENT_NUMBER == 2) { $leftA = '$-2,250.00'; $rightA = '$2,250.00'; } elsif ($PAYMENT_NUMBER == 3) { $leftA = '$-2,750.00'; $rightA = '$2,750.00'; } my(@tags) = AllFormattedLedgerTagFromTicket($ticket); my $contractName = 'FIXME'; $contractName = $paymentVals{'CONTRACTED NAME'} if defined $paymentVals{'CONTRACTED NAME'}; $contractName =~ s/^\s*//; $contractName =~ s/\s*$//; open(my $paymentValsFH, ">", File::Spec->catfile($PAYMENT_DIR, "payment-values.txt")); foreach my $key (keys %paymentVals) { print $paymentValsFH "$key:$paymentVals{$key}\n"; } close $paymentValsFH; open(my $ledgerEntryFH, ">>", File::Spec->catfile($PAYMENT_DIR, "entry.ledger")); print $ledgerEntryFH <) { if ($line =~ /Link\s+not\s+found/i) { $found = 0; } elsif ($line =~ /no\s+longer\s+depends\s+on\s+Ticket/i) { $found = 1; } last if defined $found; } close $delTravelDependsFH; if (not defined $found) { print STDERR "\"$file\": \"$ticket\": WARNING: unable to determin what to do about Travel ticket, $TRAVEL_NOTICE_TICKET... "; } else { # This means we already activiated this travel ticket, so we have to explain to the intern open(my $travelTicketCorrespondFH, "|-", $RT_CMD, 'correspond', $reimbursementTicket, '-m', '-'); print $rtCorrespondFH "Previously, you received notice about your travel stipend. Please be advised that due to your failure in your internship, you no longer have a travel stipend budget.\n\n"; print $rtCorrespondFH @dd; close $rtCorrespondFH; } system($RT_CMD, "edit", $reimbursementTicket, 'set', 'Status=open'); system($RT_CMD, "edit", $reimbursementTicket, 'set', 'Status=rejected'); } print STDERR "...done\n"; print STDERR "Waiting? "; my $x = ; } print STDERR "Old Interns, who were marked as successful (likely from previous interns) ignored: $oldInterns\n"; ############################################################################### # # Local variables: # compile-command: "perl -c rt-outreachy-payment-next.plx" # perl-indent-level: 2 # End: