#!/usr/bin/perl
#
# Author:  Petter Reinholdtsen
# Date:    2007-02-10
# License: GNU Public License v2
#
# Generate list of invoices to send.  Should generate XML, for example
# on the oioxml or the sendregning.no format.  We will need to send
# incremential invoices as well when a firm member adds more members
# to their firm.
#
# Modified by Jon Petter Bjerke
# Date:    2024-01-04
# For firm members, generate price for each person, and later deduct up to 3
# who are to included.
# 2025-02-09: Updated year for 2025
# 2026-01-09: Updated year for 2025


use strict;
use warnings;

BEGIN {
    # Add script location to include path
    my @p = split(m%/%, $0); pop @p; push @INC, join("/", @p);
}

use Getopt::Std;
use Encode;
use POSIX qw(strftime);
use Text::Wrap;
use medlemsliste;

my %members;
my %firms;
my %opts;
my $retval = 0;

my $currentperiod = current_membership_period();

my $scriptadmin = "pere\@nuug.no";
my $mailfrom = "Foreningen NUUG <medlemsregister\@rt.nuug.no>";

# i dag + 30 dager. endret til 14 dager 2017-01-27 jonp
my $duedate = strftime("%d.%m.%y", localtime(time() + 14 * 24 * 60 * 60));

my ($extra_info1, $extra_info2);
# Hardkodet ekstra info til masseutsendingen om høsten oktober/november
$extra_info1 = "For å unngå avbrudd i medlemskapet";
$extra_info2 = "må fakturaen være betalt innen forfall.";
#Encode::from_to($extra_info1 , 'iso-8859-1', 'utf8');  ## Removed, memberlist is utf-8   jonp 2022-05-07
#Encode::from_to($extra_info2 , 'iso-8859-1', 'utf8');  ## Removed, memberlist is utf-8   jonp 2022-05-07

my %memberfees =
    (
     'Æresmedlem' =>
        {fee =>    '0.00', pcode => '', pdesc => 'Æresmedlem'},

     'FIRMAMEDLEM' =>
        { fee => '3120.00', pcode => '202601',
          pdesc =>'Kontingent 2026, firmamedlem inkl. inntil 3 pers.'},

     'Medlem under bedrift' =>
        {fee => '520.00', pcode => '202602',
         pdesc => 'Kontingent 2026, ekstra person under firmamedlem'},

     'Personlig medlem' =>
        {fee => '530.00', pcode => '202603',
         pdesc => 'Kontingent 2026, personlig medlem'},

     'Studenter' =>
        {fee =>  '50.00', pcode => '202604',
         pdesc => 'Kontingent 2026, studentmedlem'},

     'LISA-medlemskap, personlig' =>
        {fee =>  '0.00', pcode => '202605',
         pdesc => 'LISA SIG 2026 (utgått)'},

     'LISA-medlemskap, student' =>
        {fee =>  '0.00', pcode => '202606',
         pdesc => 'LISA SIG 2026 (utgått)'},

     'Personlig medlem uten USENIX' =>
        {fee => '0.00', pcode => '202607',
         pdesc => 'Kontingent 2026, personlig medlem uten USENIX (utgått)'},

     'Studentmedlem uten USENIX' =>
        {fee =>  '0.00', pcode => '202609',
         pdesc => 'Kontingent 2026, studentmedlem uten USENIX (utgått)'},

     );

# The amount to invoice depend on the time of year.  This list show
# the time the price reduction changes
# my %feediscount =
#     (
#      '01-01' => '25.00',
#      '03-01' => '50.00',
#      '06-01' => '75.00',
#      '09-01' =>  '0.00',
#      '12-01' => '25.00',
#      );
my %feediscount =
    (
     '01-01' =>  '0.00',
     '07-01' => '50.00',
     );

my %blacklist = (
#    101073 => 1,
#    140192 => 1,
    );

my @fields =
    qw(CustomerNo Name EmailAddress USENIX SAGE ZUsrArbeidsgiverNo
       ZUsrArbeidsgiver Kontaktperson ZUsrMedlemstatusNo
       ZUsrMedlemstatus MobileTelephone Telephone Address1 Address2
       Address3 PostCode PostOffice Country unixlogin Informasjon
       ZUsrFakturertPeriode ZUsrFakturaReferanse ErArbeidsgiver
       ZUsrInnmeldtDato ZUsrUtmeldtDato ZBetaltTil recipientOrgNo StudentBevis EHFfaktura);

getopts("df:ip:u:Fb:", \%opts) || usage();

my $debug = 1 if $opts{d};
my $totalfee = 0;

my $itemno;

if ($opts{u}) {
    update_invoice_period($opts{u});
    exit $retval;
}

if ($opts{p}) {
    update_paid_period($opts{p});
    exit $retval;
}

# Abort if we should be invoicing for the next year  jonp 2017-09-25
# No longer valid after 2021-01-01  removed  jonp 2021-10-13
# my $thisyear  = strftime "%Y", localtime;
# my $thismonth = strftime "%m", localtime;
# if (!$opts{F} && (substr($memberfees{FIRMAMEDLEM}->{pcode},0,4) eq $thisyear) && ($thismonth >= 9)) {
#    print STDERR "ERROR: After 09-01 invoicing should be for next year\n";
#    $retval = 1;
#    exit $retval;
# }

load_memberslist($opts{f} || "medlemsliste.csv", \&process_member);
generate_invoices("-");

exit $retval;

sub usage {
    print <<EOF;
Usage: $0 [-s] [-f <filename>]
Generate XML formatted invoice data from member database.

  -f filename  load member database from filename
  -d           enable debug mode
  -i           only generate for those who have not received an invoice yet
  -u filename  Update invoiced period in member database based on provided XML
  -p filename  Update paid period in member database based on provided XML
  -F           Full prize, disable discount
  -b           to add batchId
EOF
    exit 1;
}

sub process_member {
    my $memberinfo = shift;

    for my $key (keys %{$memberinfo}) {
        # Encode::from_to( $memberinfo->{$key}, 'iso-8859-1', 'utf8');   ## Removed, memberlist is utf-8   jonp 2022-05-07
        $memberinfo->{$key} =~ s/\&/\&amp;/g if defined $memberinfo->{$key};
    }

    if ($memberinfo->{ZUsrMedlemstatus}) {
        my $customerid = $memberinfo->{CustomerNo};
        $members{$customerid} = $memberinfo;

        if ($memberinfo->{ZUsrArbeidsgiverNo}) {
            my $companyid = $memberinfo->{ZUsrArbeidsgiverNo};

            if (exists $firms{$companyid}) {
                push @{$firms{$companyid}}, $customerid;
            } else {
                $firms{$companyid} = [$customerid];
            }
        }
    }
}


sub todays_discount {
    return "0" if $opts{'F'};
    my $today = strftime "%m-%d", localtime;
    my $discount;
    for my $key (sort keys %feediscount) {
        $discount = $feediscount{$key} if ($key le $today);
    }
    return $discount;
}

sub list_header {
    return <<EOF;
<?xml version="1.0" encoding="UTF-8"?>
<!-- sendregning.no XML -->

<invoices batchId="2013_13">
EOF
}

sub list_footer {
    return <<EOF;
</invoices>
<!-- Totalt fakturert for $totalfee -->
EOF
}

sub add_invoice_line {
    my ($qty, $prodcode, $desc, $unitprice, $discount, $tax) = @_;
    my $len = length($desc);
    if ($len > 75) {
        print STDERR "Truncating '$desc' to 72 characters\n";
        $desc = substr( $desc, 0, 72 );
        $desc = "$desc...";
    }
    my $msg = "";
    $msg .= "      <line>\n";
    $itemno++;
    $msg .= "        <itemNo>$itemno</itemNo>\n";
    $msg .= "        <qty>$qty</qty>\n" unless (0==$unitprice);
    $msg .= "        <prodCode>$prodcode</prodCode>\n" if ($prodcode);
    $msg .= "        <desc>$desc</desc>\n";
    $msg .= "        <unitPrice>$unitprice</unitPrice>\n" unless (0==$unitprice);
    $msg .= "        <discount>$discount</discount>\n" unless (0==$unitprice);
    $msg .= "        <tax>$tax</tax>\n";
    $msg .= "      </line>\n";
    return $msg;
}

sub leftshift_address {
    my $memberinfo = shift;
    # left-shift addresses if the first one is empty
    if (!$memberinfo->{Address1}) {
        $memberinfo->{Address1} = $memberinfo->{Address2};
        $memberinfo->{Address2} = $memberinfo->{Address3};
        $memberinfo->{Address3} = "";
    }
    if (!$memberinfo->{Address2}) {
        $memberinfo->{Address2} = $memberinfo->{Address3};
        $memberinfo->{Address3} = "";
    }
}

sub generate_invoice {
    my $customerid = shift;
    my $memberinfo = $members{$customerid};
    return if ($members{$customerid}->{InvoiceGenerated});
    return unless ($memberinfo->{ZUsrMedlemstatus});
    my $msg;
    my @submembers;
    my $memberfee = 0;

    my $skip = 0;

    my $addrinfo = $memberinfo;

    my $new_submembers = 0;
    if ("FIRMAMEDLEM" eq $memberinfo->{ZUsrMedlemstatus}) {
        if (exists $firms{$customerid}) {
            @submembers = sort @{$firms{$customerid}};
            for my $othersid (@submembers) {

                print STDERR "Compare $currentperiod and " .
                    $members{$othersid}->{ZUsrFakturertPeriode}."\n" if $debug;

                $new_submembers += 1
                    if ($opts{i} &&
                        ( !$members{$othersid}->{ZUsrFakturertPeriode}
                          ||
                          ($currentperiod lt
                           $members{$othersid}->{ZUsrFakturertPeriode}) ));
            }
        } else {
            return;
        }
        print STDERR "New submembers : $new_submembers\n" if $debug;
    }

    return if ( $opts{i}
                && $currentperiod le $memberinfo->{ZUsrFakturertPeriode}
                && !$new_submembers);

    if ('Medlem under bedrift' eq $memberinfo->{ZUsrMedlemstatus}) {
        $addrinfo = $members{$memberinfo->{ZUsrArbeidsgiverNo}};
    }

    leftshift_address($addrinfo);

    my @infoorder = qw(Address3);

    $itemno = 0;
    $msg .= "  <invoice>\n";
    my %map =
        ('name' => 'Name',
         'city' => 'PostOffice',
         'zip' => 'PostCode');
    my %maplenlimit =
        ('name' => 42,
         'city' => 36,
         'zip'  => 8);

    for my $key (sort keys %map) {
        my $value = $addrinfo->{$map{$key}};
        if ($value) {
            if (length($value) > $maplenlimit{$key}) {
                print STDERR "error: <$key> content too long: '$value' - skipping ($customerid)\n";
                $skip = 1;
            }
            $msg .= "    <$key>$value</$key>\n";
        } else {
            print STDERR "error: Missing value for XML $key ($customerid) - skipping\n";
            $skip = 1;
        }
    }

    for my $key (@infoorder) {
        if (exists $addrinfo->{$key} && $addrinfo->{$key}) {
            print STDERR "error: Unhandled $key: $addrinfo->{$key} ($customerid)\n";
            $skip = 1;
        }
    }

    $msg .= "    <lines>\n";

    my $discount = todays_discount();

    # Handle extra persons during the year
    my $desc = $memberfees{$memberinfo->{ZUsrMedlemstatus}}->{pdesc};
    if ('Medlem under bedrift' eq $memberinfo->{ZUsrMedlemstatus}) {
        $desc = $memberfees{$memberinfo->{ZUsrMedlemstatus}}->{pdesc} .
            ": " . $memberinfo->{Name};
    }

    my $sagecount = 0;
    if (!($opts{i} && $currentperiod le $memberinfo->{ZUsrFakturertPeriode})) {
        print STDERR "Invoice line for $customerid\n" if $debug;
        $msg .= add_invoice_line(1,
                                 $memberfees{$memberinfo->{ZUsrMedlemstatus}}->{pcode},
                                 $desc,
                                 $memberfees{$memberinfo->{ZUsrMedlemstatus}}->{fee},
                                 $discount, 0);
        $memberfee += $memberfees{$memberinfo->{ZUsrMedlemstatus}}->{fee} *
            (100-$discount)/100;
        if ($memberinfo->{SAGE}) {
            $sagecount += 1;
        }
    }

    my $subfee = 0;
    my $subincluded = firmmembers_included();

    # XXX Not quite sure if 13 is the correct edge case here.
    if (99 < scalar @submembers) {
        # Compressed output
        my $type = 'Medlem under bedrift';
        my $pcode = $memberfees{$type}->{pcode};
        my $extracount = scalar @submembers - $subincluded;
        my $fee = $memberfees{$type}->{fee};
        print STDERR "Invoice line for $customerid\n" if $debug;
        $msg .= add_invoice_line($extracount,
                                 $pcode, $memberfees{$type}->{pdesc},
                                 $fee, $discount, 0);
        $subfee += $fee * $extracount * (100-$discount)/100;
        my @namelist = ();
        for my $othersid (@submembers) {
            my $namestring;
#            $namestring .= "#$othersid ";
            $namestring .= $members{$othersid}->{Name};
            if ($members{$othersid}->{SAGE}) {
                $namestring .= " (LISA)";
                $sagecount++;
            }
            push @namelist, $namestring;
            $members{$othersid}->{InvoiceGenerated} = 1;
        }
        $Text::Wrap::columns = 75;
        for my $line (split(/\n/, wrap("", "", join(", ", @namelist)))) {
            print STDERR "Invoice line for $customerid\n" if $debug;
            $msg .= add_invoice_line(1, "", $line, 0, 0, 0);
        }
    } else {
        my $numfree = 0;
        my $freetype;
        for my $othersid (@submembers) {
            my $fee = 0;
            my $extra = "";
            if (!exists $members{$othersid}->{ZUsrMedlemstatus}) {
                print STDERR "no member status type for $othersid\n";
            }
            if (!exists $memberfees{$members{$othersid}->{ZUsrMedlemstatus}}) {
                print STDERR "no memberfees found for $members{$othersid}->{ZUsrMedlemstatus} ($othersid)\n";
            }
            if (!exists $memberfees{$members{$othersid}->{ZUsrMedlemstatus}}->{fee}) {
                print STDERR "no member fee found for $othersid\n";
            }
            my $pcode = "";
            # unless ($subincluded-- > 0)    # removed jonp 2021-01-12
            unless (0) {                     # always jonp 2021-01-12
                my $type = $members{$othersid}->{ZUsrMedlemstatus};
                if ($subincluded-- > 0) {
                    $numfree++;
                    $freetype = $type;
                }
                $fee += $memberfees{$type}->{fee};
                $pcode = $memberfees{$type}->{pcode};
            };

            # Only send invoice if this member not already was invoiced
            next if ($opts{i} &&
                     $currentperiod le $members{$othersid}->{ZUsrFakturertPeriode});

            if ($members{$othersid}->{SAGE}) {
                $extra .= "/LISA";
                $sagecount++;
            }

            $subfee += $fee * (100-$discount)/100;

#        $msg .= sprintf("    %d %-40s %s\n", $othersid,
#                        $members{$othersid}->{Name}, $extra);
            print STDERR "Invoice line for $customerid\n" if $debug;
            $msg .= add_invoice_line(1, $pcode,
                                     "$othersid$extra " .
                                     $members{$othersid}->{Name} .
                                     " " .
                                     $members{$othersid}->{EmailAddress} .
                                     "",
                                     $fee, $discount, 0);  # jonp 2024-01-20 removed <>
            $members{$othersid}->{InvoiceGenerated} = 1;
        }
        # Deduct number of included submembers, but not more than all                jonp 2021-01-12
        if ($numfree > 0) {                                                        # jonp 2021-01-12
            my $fee = $memberfees{$freetype}->{fee};
            $msg .= add_invoice_line($numfree, $memberfees{$freetype}->{pcode},    # jonp 2021-01-16
                                     "Inkluderte personer",                        # jonp 2021-01-12
                                     -$fee,                                        # jonp 2021-01-16
                                     $discount, 0);                                # jonp 2021-01-12
            $subfee -= $numfree * $fee * (100-$discount)/100;                      # jonp 2021-01-12
        }                                                                          # jonp 2021-01-12
    }
    if ($sagecount) {
        my $sagestr = "LISA-medlemskap";
        if ("Studenter" eq $memberinfo->{ZUsrMedlemstatus}) {
            $sagestr .= ", student";
        } else {
            $sagestr .= ", personlig";
        }
        print STDERR "Invoice line for $customerid\n" if $debug;
        $msg .= add_invoice_line($sagecount, $memberfees{$sagestr}->{pcode},
                                 $memberfees{$sagestr}->{pdesc},
                                 $memberfees{$sagestr}->{fee},
                                 $discount, 0);
        $memberfee += $sagecount * $memberfees{$sagestr}->{fee} * (100-$discount)/100;
    }



    # If there is room, include the email address used for invoicing
    # Not used - added to invoiceText instead  2021-01-12 jonp
    # if ($itemno < 15) {
    #    my $email = $memberinfo->{EmailAddress};
    #    print STDERR "Invoice line for $customerid\n" if $debug;
    #    $msg .= add_invoice_line(0, 0, "Epostadresse: '$email'", 0, 0, 0);
    # }

    if (0 && $discount <= 0 && !$opts{'F'}) {  # disabled for now. To enable remove '0 &&'  2021-01-03 jonp
        print STDERR "Invoice line for $customerid\n" if $debug;
        $msg .= add_invoice_line(0, 0, "", 0, 0, 0);
        $msg .= add_invoice_line(0, 0, $extra_info1, 0, 0, 0);
        $msg .= add_invoice_line(0, 0, $extra_info2, 0, 0, 0);
    }
    my $skipfatal = 1;
    # Increased allowed number of lines from 18 to 28, after configuring sendregning to not include giro  jonp 2021-10-31
    if ($itemno > 28) {
        print STDERR "error: Too many invoice lines: $itemno > 28 ($customerid) - skipping\n";
        $skip = 1;
        $skipfatal = 0;
    }

    $memberfee += $subfee if $subfee > 0;
    $totalfee += $memberfee;

    $msg .= "    </lines>\n";
    $msg .= "    <!--  Kontingent: $memberfee -->\n";

    $msg .= "    <optional>\n";

    # Included mobile and phone, mostly for Vipps   jonp 2021-02-02
    my %optmap =
        ('recipientNo' => 'CustomerNo',
         'address1' => 'Address1',
         'address2' => 'Address2',
         'country' => 'Country',
         'yourRef' => 'ZUsrFakturaReferanse',
         'email' => 'EmailAddress',
         'mobile' => 'MobileTelephone',
         'phone' => 'Telephone',
	 'recipientOrgNo' => 'recipientOrgNo'
		 );
    # Included mobile and phone, mostly for Vipps   jonp 2021-02-02
    my %optlenlimit =
        ('recipientNo' => 32,
         'address1'    => 42,
         'address2'    => 42,
         'country'     => 42,
         'yourRef'     => 30,
         'email'       => 64,
         'mobile'      => 16,
         'phone'       => 16,
		 'recipientOrgNo'       => 32,
         );

    if (! $addrinfo->{MobileTelephone}) {       # jonp 2021-10-31  avoid blank mobile to force update in sendregning
        $addrinfo->{MobileTelephone} = "-";
    }
    if (! $addrinfo->{Telephone}) {             # jonp 2021-10-31  avoid blank telephone
        $addrinfo->{Telephone} = "-";
    }

    for my $key (sort keys %optmap) {
        my $value = $addrinfo->{$optmap{$key}};
        if ($value) {
            if (length($value) > $optlenlimit{$key}) {
                print STDERR "error: <$key> content too long: '" . $value . "' - skipping ($customerid)\n";
                $skip = 1;
            } else {
                $msg .= "      <$key>$value</$key>\n";
            }
        }
    }
    my $value = $addrinfo->{'Kontaktperson'};
    my $email = $addrinfo->{'EmailAddress'};  # jonp 2021-01-12
    $msg .= "      <invoiceText>Kontakt: $value  $email</invoiceText>\n"  # jonp 2021-01-12 2024-01-20 removed <>
        if $value || $email;    # jonp 2021-01-12

    $msg .= "      <dueDate>$duedate</dueDate>\n" if $duedate;
    $msg .= "    </optional>\n";

    if (!$addrinfo->{EmailAddress} ||
        # Unngå firma-epostlister som abonnerer på medlemmer@,
        # da disse antagelig går til bedriftsinterne nyhetslister
        # og ikke til fakturaavdelingen.
        ( $addrinfo->{EmailAddress} && $addrinfo->{ErArbeidsgiver} &&
          $addrinfo->{Informasjon})
        ) {
        $msg .= "    <shipment>PAPER</shipment>\n";
    } else {
        $msg .= "    <shipment>EMAIL\n";
        $msg .= "      <emailaddresses>\n";
        $msg .= "        <email>" . $addrinfo->{EmailAddress} . "</email>\n";
        $msg .= "      </emailaddresses>\n";
        $msg .= "    </shipment>\n";
    }

    $msg .= "  </invoice>\n";

    $msg .= "\n";

    $members{$customerid}->{InvoiceGenerated} = 1;

    if ($skip) {
        $retval = 1 if ($skipfatal);
        return "";
    }

    return $msg;
}

sub generate_invoices {
    print list_header();
    # Process firms first, to handle all their sub-members
    for my $customerid ((sort keys %firms), (sort keys %members)) {
        my $msg;
        my $xml = generate_invoice($customerid);
        next if exists $blacklist{$customerid};
        print $xml if $xml;
    }
    print list_footer();
}

sub update_invoice_period {
    my ($filename) = @_;
    # Update member database with invoice period, based on the
    # provided XML file.

    my %invoiced;
    # Load IDs from XML file
    open(XML, "<", $filename) || die "Unable to load file " . $filename;
    while (<XML>) {
        chomp;
        $invoiced{$1} = 1 if m%<recipientNo>(\d+)</recipientNo>%;
    }
    close(XML);

    my $headerprinted = 0;

    # Update all entries with IDs (both companies and persons)
    # and print the new file
    load_memberslist($opts{f} || "medlemsliste.csv",
                     sub {
        my $memberinfo = shift;
        if (exists $invoiced{$memberinfo->{CustomerNo}}
            || exists $invoiced{$memberinfo->{ZUsrArbeidsgiverNo}}) {
            $memberinfo->{ZUsrFakturertPeriode} = $currentperiod;
        }
        unless ($headerprinted) {
            print join("\t", @fields),"\n";
            $headerprinted = 1;
        }
        my @values;
        for my $key (@fields) {
            my $value = $memberinfo->{$key};
            $value = "" unless defined $value;
            push(@values,  $value);
        }
        print join("\t", @values),"\n";
    });
}

sub update_paid_period {
    my ($filename) = @_;
    # Update member database with paid period, based on the
    # provided XML file and invoiced period.

    my %paid;
    # Load IDs from XML file
    open(XML, "<", $filename) || die "Unable to load file " . $filename;
    while (<XML>) {
        chomp;
        if (m%<recipientNo>(\d+)</recipientNo>%) {
            print STDERR "info: Found payment for CustomerNo $1\n" if $opts{d};
            $paid{$1} = 1;
        }
    }
    close(XML);

    my $headerprinted = 0;

    # Update all entries with IDs (both companies and persons)
    # and print the new file
    load_memberslist($opts{f} || "medlemsliste.csv", sub {
        my $memberinfo = shift;
        if (exists $paid{$memberinfo->{CustomerNo}}
            || exists $paid{$memberinfo->{ZUsrArbeidsgiverNo}}) {
            print STDERR "info: replacing paid date for CustomerNo " .
                $memberinfo->{CustomerNo} . " with '" .
                $memberinfo->{ZUsrFakturertPeriode} .
                "'\n" if $opts{d};
            $memberinfo->{ZBetaltTil} =
                $memberinfo->{ZUsrFakturertPeriode};
        }
        unless ($headerprinted) {
            print join("\t", @fields),"\n";
            $headerprinted = 1;
        }
        my @values;
        for my $key (@fields) {
            my $value = $memberinfo->{$key};
            $value = "" unless defined $value;
            push(@values,  $value);
        }
        print join("\t", @values),"\n";
    });
}
