From 0ee0f810245229fc748526210952fa834824cb1a Mon Sep 17 00:00:00 2001
From: Erik Thuning <boooink@gmail.com>
Date: Fri, 29 Apr 2022 15:58:38 +0200
Subject: [PATCH] Changed cron behaviour to warn users 3 days before their
 loans end

---
 include/Cron.php | 221 +++++++++++++++++++++++++++++++++--------------
 include/Kvs.php  |   4 +-
 include/Ldap.php |  25 +++---
 include/Loan.php |  14 ++-
 include/User.php |  10 +++
 5 files changed, 194 insertions(+), 80 deletions(-)

diff --git a/include/Cron.php b/include/Cron.php
index 7ca58e8..ad6d26b 100644
--- a/include/Cron.php
+++ b/include/Cron.php
@@ -6,22 +6,26 @@ class Cron {
     private $kvs;
     private $ldap;
     public function __construct($sender, $error) {
-        $this->now = time();
+        $this->now = new DateTimeImmutable();
         $this->sender = $sender;
         $this->error = $error;
+        $warn_time = DateInterval::createFromDateString('3 days');
+        $this->warn_date = $this->now->add($warn_time);
+        $this->run_interval = DateInterval::createFromDateString('1 day');
         $this->kvs = new Kvs();
         $this->ldap = new Ldap();
     }
 
     public function run() {
-        $lastrun = $this->kvs->get_value('lastrun');
-        $interval = 3600*24; //1 day in seconds
-        
-        if($lastrun && $this->now - $lastrun < $interval) {
+        $lastrun = $this->kvs->get_value('lastrun', 0);
+        $nextrun = $this->now
+                        ->setTimestamp($lastrun)
+                        ->add($this->run_interval);
+        if($nextrun > $this->now) {
             return;
         }
-        $this->kvs->set_key('lastrun', $this->now);
-        
+        $this->kvs->set_key('lastrun', $this->now->getTimestamp());
+
         $users = get_items('user');
         foreach($users as $user) {
             $this->check_loans($user);
@@ -29,89 +33,174 @@ class Cron {
     }
 
     private function check_loans($user) {
+        $expiring = $user->get_expiring_loans($this->warn_date);
         $overdue = $user->get_overdue_loans();
-        if($overdue) {
-            $this->send_reminder($user, $overdue);
+        if($expiring || $overdue) {
+            $this->send_reminder($user, $expiring, $overdue);
         }
     }
-    
-    private function send_reminder($user, $loans) {
-        $subject_template = "DSV Helpdesk: Du har ¤count¤ ¤late¤ lån";
-        $reminder_template_sv = "¤brand¤ ¤name¤, försenad sedan ¤due¤\n";
-        $reminder_template_en = "¤brand¤ ¤name¤, late since ¤due¤\n";
-        $message_template = <<<EOF
-Hej ¤name¤
 
-Vi vill påminna dig om att ditt lån har gått ut på följande ¤product_sv¤:
+    private function make_subject($num_expiring, $num_expired) {
+        $subject = "DSV Helpdesk: ";
+        $messages = array();
+        if($num_expiring > 0) {
+            $messages[] = $num_expiring." utgående lån";
+        }
+        if($num_expired > 1) {
+            $messages[] = $num_expired." försenade lån";
+        } elseif($num_expired > 0) {
+            $messages[] = $num_expired." försenat lån";
+        }
+        return $subject.implode(" och ", $messages);
+    }
 
-¤list_sv¤
+    private function make_expiring_notice($lang, $expiring) {
+        if(!$expiring) {
+            return '';
+        }
+        $days = $this->warn_date->d;
+        switch($lang) {
+            case 'sv':
+                $msg = "Följande lån går ut om mindre än ".$days." dagar:";
+                $itemglue = ", går ut ";
+                break;
+            case 'en':
+                if(count($expiring) == 1) {
+                    $loanstr = "loan expires";
+                } else {
+                    $loanstr = "loans expire";
+                }
+                $msg = "The following ".$loanstr." in less than ".$days." days:";
+                $itemglue = ", expires on ";
+                break;
+            default:
+                throw new Exception("Invalid language: ".$lang);
+        }
+        $msg .= "\n\n";
+        foreach($expiring as $loan) {
+            $product = $loan->get_product();
+            $serial = $product->get_serial();
+            $brand = $product->get_brand();
+            $name = $product->get_name();
+            $endtime = format_date($loan->get_endtime());
+            $msg .= $serial.": ".$brand." ".$name.$itemglue.$endtime;
+        }
+        return $msg;
+    }
 
-Vänligen återlämna ¤it_sv¤ till Helpdesk så snart som möjligt, alternativt svara på det här meddelandet för att förlänga ¤loan_sv¤.
+    private function make_overdue_notice($lang, $overdue) {
+        if(!$overdue) {
+            return '';
+        }
+        switch($lang) {
+            case 'sv':
+                $msg = "Följande lån har gått ut:";
+                $itemglue = ", gick ut ";
+                break;
+            case 'en':
+                if(count($overdue) == 1) {
+                    $msg = "The following loan has expired:";
+                } else {
+                    $msg = "The following loans have expired:";
+                }
+                $itemglue = ", expired on ";
+                break;
+            default:
+                throw new Exception("Invalid language: ".$lang);
+        }
+        $msg .= "\n\n";
+        foreach($overdue as $loan) {
+            $product = $loan->get_product();
+            $serial = $product->get_serial();
+            $brand = $product->get_brand();
+            $name = $product->get_name();
+            $endtime = format_date($loan->get_endtime());
+            $msg .= $serial.": ".$brand." ".$name.$itemglue.$endtime;
+        }
+        return $msg;
+    }
+
+    private function make_return_info($lang, $count) {
+        switch($lang) {
+            case 'sv':
+                if($count > 1) {
+                    $loan = "lånen";
+                    $product = "artiklarna";
+                } else {
+                    $loan = "lånet";
+                    $product = "artikeln";
+                }
+                return "Vänligen kontakta Helpdesk för att förlänga $loan eller lämna tillbaka $product.";
+                break;
+            case 'en':
+                if($count > 1) {
+                    $loan = "loans";
+                    $product = "items";
+                } else {
+                    $loan = "loan";
+                    $product = "item";
+                }
+                return "Please contact Helpdesk in order to extend the $loan or return the $product.";
+                break;
+            default:
+                throw new Exception("Invalid language: ".$lang);
+        }
+
+    }
+
+    private function send_reminder($user, $expiring, $expired) {
+        $uid = $user->get_name();
+        $name = $this->ldap->get_firstname($uid);
+
+        $subject = $this->make_subject(count($expiring), count($expired));
+
+        $info_sv = array();
+        $info_sv[] = $this->make_expiring_notice('sv', $expiring);
+        $info_sv[] = $this->make_overdue_notice('sv', $expired);
+        $info_sv = implode("\n\n", $info_sv);
+        $returns_sv = $this->make_return_info(
+            'sv', count($expiring) + count($expired));
+
+        $info_en = array();
+        $info_en[] = $this->make_expiring_notice('en', $expiring);
+        $info_en[] = $this->make_overdue_notice('en', $expired);
+        $info_en = implode("\n\n", $info_en);
+        $returns_en = $this->make_return_info(
+            'en', count($expiring) + count($expired));
+
+        $message = <<<EOF
+Hej $name!
+
+Det här är en automatisk påminnelse om lånade artiklar från Helpdesk.
+
+$info_sv
+
+$returns_sv
 
 ----
 
-We would like to remind you that your loan has expired on the following ¤product_en¤:
+This is an automated reminder regarding items on loan from Helpdesk.
 
-¤list_en¤
+$info_en
 
-Please return ¤it_en¤ to the Helpdesk as soon as possible, or reply to this message in order to extend the ¤loan_en¤.
+$returns_en
 
 Mvh
 DSV Helpdesk
 helpdesk@dsv.su.se
 08 - 16 16 48
+
 EOF;
-
-        $overdue_count = count($loans);
-        $reminder_list_sv = '';
-        $reminder_list_en = '';
-        $late = 'försenat';
-        $product_sv = 'artikel';
-        $product_en = 'product';
-        $it_sv = 'den';
-        $it_en = 'it';
-        $loan_sv = 'lånet';
-        $loan_en = 'loan';
-        if($overdue_count > 1) {
-            $late = 'försenade';
-            $product_sv = 'artiklar';
-            $product_en = 'products';
-            $it_sv = 'dem';
-            $it_en = 'them';
-            $loan_sv = 'lånen';
-            $loan_en = 'loans';
-        }
-        foreach($loans as $loan) {
-            $replacements = array('name'  => $loan->get_product()->get_name(),
-                                  'brand' => $loan->get_product()->get_brand(),
-                                  'due'   => format_date($loan->get_endtime()));
-            $reminder_list_sv .= replace($replacements, $reminder_template_sv);
-            $reminder_list_en .= replace($replacements, $reminder_template_en);
-        }
-
-        $subject = replace(array('count' => $overdue_count,
-                                 'late'  => $late), $subject_template);
-        $message = replace(array('name'       => $user->get_displayname($this->ldap),
-                                 'list_sv'    => $reminder_list_sv,
-                                 'product_sv' => $product_sv,
-                                 'it_sv'      => $it_sv,
-                                 'loan_sv'    => $loan_sv,
-                                 'list_en'    => $reminder_list_en,
-                                 'product_en' => $product_en,
-                                 'it_en'      => $it_en,
-                                 'loan_en'    => $loan_en),
-                           $message_template);
-
         try {
-            mb_send_mail($user->get_email($this->ldap),
+            mb_send_mail($this->ldap->get_user_email($uid),
                          $subject,
                          $message,
                          'From: '.$this->sender);
         } catch(Exception $e) {
+            error_log($e->message);
             mb_send_mail($this->error,
                          "Kunde inte skicka påminnelse",
-                         "Påminnelse kunde inte skickas till "
-                         .$user->get_name());
+                         "Påminnelse kunde inte skickas till ".$uid);
         }
     }
 }
diff --git a/include/Kvs.php b/include/Kvs.php
index e80edf8..2900dcd 100644
--- a/include/Kvs.php
+++ b/include/Kvs.php
@@ -16,11 +16,11 @@ class Kvs {
         return array_keys($this->items);
     }
     
-    public function get_value($key) {
+    public function get_value($key, $default=null) {
         if(isset($this->items[$key])) {
             return $this->items[$key];
         }
-        return null;
+        return $default;
     }
     
     public function set_key($key, $value) {
diff --git a/include/Ldap.php b/include/Ldap.php
index 00e17b4..f43d7f9 100644
--- a/include/Ldap.php
+++ b/include/Ldap.php
@@ -2,7 +2,7 @@
 class Ldap {
     private $conn;
     private $base_dn = "dc=su,dc=se";
-    
+
     public function __construct() {
         $this->conn = ldap_connect('ldaps://ldap.su.se');
         ldap_set_option($this->conn, LDAP_OPT_PROTOCOL_VERSION, 3);
@@ -13,23 +13,26 @@ class Ldap {
         $result = ldap_search($this->conn, $this->base_dn, $term, $attributes);
         return ldap_get_entries($this->conn, $result);
     }
-    
-    public function get_user($uid) {
-        $data = $this->search("uid=$uid", 'cn', 'uid');
+
+    public function get_attribute($uid, $attribute) {
+        $data = $this->search("uid=$uid", $attribute);
         if($data['count'] !== 1) {
             $err = "LDAP search for '$uid' did not return exactly one result";
             throw new Exception($err);
         }
-        return $data[0]['cn'][0];
+        return $data[0][strtolower($attribute)][0];
+    }
+
+    public function get_user($uid) {
+        return $this->get_attribute($uid, 'cn');
+    }
+
+    public function get_firstname($uid) {
+        return $this->get_attribute($uid, 'givenName');
     }
 
     public function get_user_email($uid) {
-        $data = $this->search("uid=$uid", 'mail', 'uid');
-        if($data['count'] !== 1) {
-            $err = "LDAP search for '$uid' did not return exactly one result";
-            throw new Exception($err);
-        }
-        return $data[0]['mail'][0];
+        return $this->get_attribute($uid, 'mail');
     }
 
     public function search_email($email) {
diff --git a/include/Loan.php b/include/Loan.php
index 9bf38c8..08311b7 100644
--- a/include/Loan.php
+++ b/include/Loan.php
@@ -54,7 +54,7 @@ class Loan extends Event {
         $this->endtime = $ts;
         return true;
     }
-    
+
     public function end() {
         $now = time();
         $query = prepare('update `event` set `returntime`=? where `id`=?');
@@ -75,6 +75,18 @@ class Loan extends Event {
         return false;
     }
 
+    public function expires_before($datetime) {
+        if($this->returntime !== null) {
+            return false;
+        }
+        $endtime = new DateTime();
+        $endtime->setTimestamp($this->endtime);
+        if(!$this->is_overdue() && $endtime < $datetime) {
+            return true;
+        }
+        return false;
+    }
+
     public function get_status() {
         if($this->is_overdue()) {
             return 'overdue_loan';
diff --git a/include/User.php b/include/User.php
index d77475f..a34ca03 100644
--- a/include/User.php
+++ b/include/User.php
@@ -189,5 +189,15 @@ class User extends Entity {
         }
         return $overdue;
     }
+
+    public function get_expiring_loans($end_date) {
+        $expiring = array();
+        foreach($this->get_loans('active') as $loan) {
+            if($loan->expires_before($end_date)) {
+                $expiring[] = $loan;
+            }
+        }
+        return $expiring;
+    }
 }
 ?>