diff --git a/README.md b/README.md index 7ec059b..35d272b 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,7 @@ and a key providing the cost of the item. The price can be provided in one of th to working out what the hourly rate should be in a given month and multiplying it by the number of hours worked in that month * :function - an S-expression describing how to calculate the net value. Only numbers, basic mathematical - operations (+, -, /, *) and timesheet specific variables are supported (:worked, :required, - :to, :from). + operations (+, -, /, *) and timesheet specific variables are supported (:worked, :required). Examples: diff --git a/src/invoices/calc.clj b/src/invoices/calc.clj new file mode 100644 index 0000000..14480df --- /dev/null +++ b/src/invoices/calc.clj @@ -0,0 +1,40 @@ +(ns invoices.calc) + +(defn round [val] + (double (/ (Math/round (* val 100.0)) 100))) + +(defn vat [{netto :netto vat-level :vat}] + (if-not vat-level 0 (* netto (/ vat-level 100)))) + +(defn brutto [{netto :netto :as item}] (round (+ netto (vat item)))) + +(defn parse-custom + "Parse the given function definition and execute it with the given `worklog`." + [work-log func] + (cond + (and (list? func) (some #{(first func)} '(+ - * /))) (apply (resolve (first func)) + (map (partial parse-custom work-log) (rest func))) + (some #{func} '(:worked :required)) (func work-log) + (or (list? func) (not (number? func))) (throw (IllegalArgumentException. (str "Invalid functor provided: " (first func)))) + :else func)) + +(defn calc-part-time [{worked :worked total :required} {base :base per-day :per-day}] + (let [hourly (/ (* base 8) (* total per-day))] + (float (* hourly worked)))) + +(defn calc-hourly [{worked :worked} {hourly :hourly}] + (* worked hourly)) + +(defn calc-custom [worked {function :function}] + (parse-custom worked function)) + + +(defn set-price + "Set the net price for the given item, calculating it from a worklog if applicable." + [worked item] + (cond + (contains? item :function) (assoc item :netto (calc-custom worked item)) + (contains? item :hourly) (assoc item :netto (calc-hourly worked item)) + (contains? item :base) (assoc item :netto (calc-part-time worked item)) + (not (contains? item :netto)) (assoc item :netto 0) + :else item)) diff --git a/src/invoices/core.clj b/src/invoices/core.clj index 950de09..ed5a5aa 100644 --- a/src/invoices/core.clj +++ b/src/invoices/core.clj @@ -1,7 +1,9 @@ (ns invoices.core (:require [invoices.pdf :as pdf] [invoices.settings :refer [invoices]] - [invoices.jira :refer [prev-timesheet prev-month]] + [invoices.jira :refer [prev-timesheet]] + [invoices.time :refer [prev-month last-working-day date-applies?]] + [invoices.calc :refer [set-price]] [clojure.tools.cli :refer [parse-opts]] [clojure.string :as str] [clojure.java.shell :refer [sh]] @@ -11,33 +13,6 @@ (defn invoice-number [when number] (->> [(or number 1) (-> when .getMonthValue) (-> when .getYear)] (map str) (str/join "/"))) -(defn parse-custom [work-log func] - (cond - (and (list? func) (some #{(first func)} '(+ - * /))) (apply (resolve (first func)) - (map (partial parse-custom work-log) (rest func))) - (list? func) (throw (IllegalArgumentException. (str "Invalid functor provided: " (first func)))) - (some #{func} '(:worked :required :to :from)) (func work-log) - :else func)) - -(defn calc-part-time [{worked :worked total :required} {base :base per-day :per-day}] - (let [hourly (/ (* base 8) (* total per-day))] - (float (* hourly worked)))) - -(defn calc-hourly [{worked :worked} {hourly :hourly}] - (* worked hourly)) - -(defn calc-custom [worked {function :function}] - (parse-custom worked function)) - - -(defn set-price [worked item] - (cond - (contains? item :function) (assoc item :netto (calc-custom worked item)) - (contains? item :hourly) (assoc item :netto (calc-hourly worked item)) - (contains? item :base) (assoc item :netto (calc-part-time worked item)) - (not (contains? item :netto)) (assoc item :netto 0) - :else item)) - (defn send-email [to from {smtp :smtp} invoice] (when (not-any? nil? [to from smtp invoice]) (->> @@ -62,17 +37,13 @@ (defn run-callbacks [invoice callbacks] (doall (map (partial run-callback invoice) callbacks))) -(defn date-applies? [when {to :to from :from}] - (and (or (nil? to) (-> when .toString (compare to) (< 0))) - (or (nil? from) (-> when .toString (compare from) (>= 0))))) - (defn render-month [when {seller :seller buyer :buyer items :items creds :credentials font-path :font-path} number] (pdf/render seller buyer (->> items (filter (partial date-applies? when)) (map (partial set-price (prev-timesheet when creds)))) - (pdf/last-working-day when) + (last-working-day when) (invoice-number when number) font-path)) diff --git a/src/invoices/jira.clj b/src/invoices/jira.clj index d2f1ea9..2d0d723 100644 --- a/src/invoices/jira.clj +++ b/src/invoices/jira.clj @@ -1,5 +1,6 @@ (ns invoices.jira - (:require [clj-http.client :as client])) + (:require [clj-http.client :as client] + [invoices.time :refer [last-day prev-month]])) (defn tempo [{tempo-token :tempo-token} endpoint params] (-> (str "https://api.tempo.io/core/3" endpoint) @@ -16,9 +17,6 @@ (defn timesheet [{spent :timeSpentSeconds required :requiredSeconds period :period}] (merge {:worked (/ spent 3600) :required (/ required 3600)} period)) -(defn last-day [when] (-> when (.withDayOfMonth 1) (.plusMonths 1) (.minusDays 1))) -(defn prev-month [when] (-> when (.withDayOfMonth 1) (.minusMonths 1))) - (defn get-timesheet [who when credentials] (->> {"from" (-> when (.withDayOfMonth 1) .toString) "to" (-> when last-day .toString)} (tempo credentials (str "/timesheet-approvals/user/" who)) diff --git a/src/invoices/pdf.clj b/src/invoices/pdf.clj index 105b365..a8257db 100644 --- a/src/invoices/pdf.clj +++ b/src/invoices/pdf.clj @@ -1,17 +1,10 @@ (ns invoices.pdf (:require [clj-pdf.core :refer [pdf]] + [invoices.time :refer [skip-days-off last-working-day]] + [invoices.calc :refer [brutto vat round]] [clojure.string :as str]) (:import [java.awt Font])) - -(defn round [val] - (float (/ (Math/round (* val 100.0)) 100))) - -(defn vat [{netto :netto vat-level :vat}] - (if-not vat-level 0 (* netto (/ vat-level 100)))) - -(defn brutto [{netto :netto :as item}] (round (+ netto (vat item)))) - (defn format-total [items getter] [:cell {:background-color [216 247 249]} (->> items (map getter) (reduce +) round str)]) @@ -37,58 +30,55 @@ (str/replace #"[ -/]" "_")))) +(defn pdf-body + "Generate the actual pdf body" + [title seller buyer items when number font] + [{:title title + :right-margin 50 + :author (:name seller) + :bottom-margin 10 + :left-margin 10 + :top-margin 20 + :font font + :size "a4" + :footer "page"} + + [:heading "Faktura"] + [:spacer] + [:paragraph (str "Nr " number)] + [:spacer 2] + + [:table {:border false :padding 0 :spacing 0 :num-cols 6} + [(format-param "sprzedawca") (format-value (:name seller)) (format-param "nabywca") (format-value (:name buyer))] + [(format-param "adres") (format-value (:address seller)) (format-param "adres") (format-value (:address buyer))] + [(format-param "nip") (format-value (:nip seller)) (format-param "nip") (format-value (:nip buyer))] + (clojure.core/when (:phone seller) [(format-param "numer telefonu") (format-value (:phone seller))])] + + [:spacer] + [:line] + [:table {:border false :padding 0 :spacing 0 :num-cols 6} + [(format-param "data wystawienia") (-> when .toString format-value) (format-param "sposób płatności") (format-value "Przelew")] + [(format-param "data sprzedaży") (-> when .toString format-value) (format-param "bank") (format-value (:bank seller))] + [(format-param "termin płatności") (-> when (.plusDays 14) .toString format-value) (format-param "numer konta") (format-value (:account seller))]] + + (concat + [:table + {:header [{:background-color [216 247 249]} "Lp." [:cell {:colspan 4} "Nazwa"] "Ilość" "Cena netto" "Stawka VAT" "Kwota VAT" "Wartość brutto"] + :num-cols 10}] + (->> items + (map format-product) + (map-indexed #(concat [(inc %1)] %2))) + [[[:cell {:background-color [84 219 229] :colspan 5 :align :center} "Razem"] + (format-total items (constantly 1)) + (format-total items :netto) + "" + (format-total items vat) + (format-total items brutto)]])]) + + (defn render [seller buyer items when number & [font-path]] (let [title (get-title (:team seller) (:name seller) number)] (println " -" title) - (pdf - [{:title title - :right-margin 50 - :author (:name seller) - :bottom-margin 10 - :left-margin 10 - :top-margin 20 - :font (clojure.core/when font-path{:encoding :unicode :ttf-name font-path}) - :size "a4" - :footer "page"} - - [:heading "Faktura"] - [:spacer] - [:paragraph (str "Nr " number)] - [:spacer 2] - - [:table {:border false :padding 0 :spacing 0 :num-cols 6} - [(format-param "sprzedawca") (format-value (:name seller)) (format-param "nabywca") (format-value (:name buyer))] - [(format-param "adres") (format-value (:address seller)) (format-param "adres") (format-value (:address buyer))] - [(format-param "nip") (format-value (:nip seller)) (format-param "nip") (format-value (:nip buyer))] - (clojure.core/when (:phone seller) [(format-param "numer telefonu") (format-value (:phone seller))])] - - [:spacer] - [:line] - [:table {:border false :padding 0 :spacing 0 :num-cols 6} - [(format-param "data wystawienia") (-> when .toString format-value) (format-param "sposób płatności") (format-value "Przelew")] - [(format-param "data sprzedaży") (-> when .toString format-value) (format-param "bank") (format-value (:bank seller))] - [(format-param "termin płatności") (-> when (.plusDays 14) .toString format-value) (format-param "numer konta") (format-value (:account seller))]] - - (concat - [:table - {:header [{:background-color [216 247 249]} "Lp." [:cell {:colspan 4} "Nazwa"] "Ilość" "Cena netto" "Stawka VAT" "Kwota VAT" "Wartość brutto"] - :num-cols 10}] - (->> items - (map format-product) - (map-indexed #(concat [(inc %1)] %2))) - [[[:cell {:background-color [84 219 229] :colspan 5 :align :center} "Razem"] - (format-total items (constantly 1)) - (format-total items :netto) - "" - (format-total items vat) - (format-total items brutto)]])] - (str title ".pdf")) + (pdf (pdf-body title seller buyer items when number (clojure.core/when font-path{:encoding :unicode :ttf-name font-path})) + (str title ".pdf")) title)) - - -(defn skip-days-off [when] - (if (some #{(.getDayOfWeek when)} [java.time.DayOfWeek/SATURDAY java.time.DayOfWeek/SUNDAY]) - (skip-days-off (.minusDays when 1)) when)) - -(defn last-working-day [when] - (-> when (.withDayOfMonth 1) (.plusMonths 1) (.minusDays 1) skip-days-off)) diff --git a/src/invoices/time.clj b/src/invoices/time.clj new file mode 100644 index 0000000..ad3e61b --- /dev/null +++ b/src/invoices/time.clj @@ -0,0 +1,27 @@ +(ns invoices.time) + + +(defn last-day + "Get the last day of the month of the given `date`" + [date] (-> date (.withDayOfMonth 1) (.plusMonths 1) (.minusDays 1))) + +(defn prev-month + "Get the first day of the month preceeding the given `date`" + [date] (-> date (.withDayOfMonth 1) (.minusMonths 1))) + +(defn skip-days-off + "Return the first day before (and including) `date` that isn't a day off." + [date] + (if (some #{(.getDayOfWeek date)} [java.time.DayOfWeek/SATURDAY java.time.DayOfWeek/SUNDAY]) + (skip-days-off (.minusDays date 1)) date)) + +(defn last-working-day + "Get the last working day of `date`'s month." + [date] + (-> date (.withDayOfMonth 1) (.plusMonths 1) (.minusDays 1) skip-days-off)) + +(defn date-applies? + "Return whether the provided `date` is between the provided :to and :from dates." + [date {to :to from :from}] + (and (or (nil? to) (-> date .toString (compare to) (< 0))) + (or (nil? from) (-> date .toString (compare from) (>= 0))))) diff --git a/test/invoices/calc_test.clj b/test/invoices/calc_test.clj new file mode 100644 index 0000000..86c4d92 --- /dev/null +++ b/test/invoices/calc_test.clj @@ -0,0 +1,110 @@ +(ns invoices.calc-test + (:require [clojure.test :refer :all] + [invoices.calc :refer :all])) + + +(deftest test-round + (testing "Rounding to 2 decimal points" + (is (== (round 10) 10.0)) + (is (== (round 10.1) 10.1)) + (is (== (round 10.12) 10.12)) + (is (== (round 10.123) 10.12)) + (is (== (round 10.1234) 10.12))) + + (testing "Rounding is correct" + (is (== (round 10.5) 10.5)) + (is (== (round 10.119) 10.12)) + (is (== (round 10.155) 10.15)) + (is (== (round 10.1251) 10.13)))) + + +(deftest test-vat + (testing "Check vat calculations" + (is (= (vat {:netto 1000 :vat 23}) 230)) + (is (= (vat {:netto 1000 :vat 8}) 80)) + (is (= (vat {:netto 1000 :vat 0}) 0)) + (is (= (vat {:netto 0 :vat 23}) 0)) + (is (= (vat {:netto 1000 :vat -8}) -80)))) + +(deftest test-brutto + (testing "Check whether calculating brutto works" + (is (= (brutto {:netto 1000 :vat 23}) 1230.0)) + (is (= (brutto {:netto 1000 :vat 8}) 1080.0)) + (is (= (brutto {:netto 1000 :vat 0}) 1000.0)) + (is (= (brutto {:netto 0 :vat 23}) 0.0)) + + ; negative VAT, coz why not? + (is (= (brutto {:netto 1000 :vat -23}) 770.0)))) + + +(deftest test-parse-custom + (testing "Check that the specified operators are allowed" + (is (= (parse-custom {} '(+ 1 2)) 3)) + (is (= (parse-custom {} '(- 1 2)) -1)) + (is (= (parse-custom {} '(* 4 2)) 8)) + (is (= (parse-custom {} '(/ 13 2)) 13/2))) + + (testing "Check that non specified operators cause errors" + (is (thrown? java.lang.IllegalArgumentException (parse-custom {} '(> 1 2)))) + (is (thrown? java.lang.IllegalArgumentException (parse-custom {} '(< 1 2)))) + (is (thrown? java.lang.IllegalArgumentException (parse-custom {} '(map 1 2))))) + + (testing "Check that worklog values get used" + (is (= (parse-custom {:worked 12} '(+ :worked 2)) 14)) + (is (= (parse-custom {:required 32} '(- :required 2)) 30))) + + (testing "Check error raised if non worklog keys provided" + (is (thrown? java.lang.IllegalArgumentException (parse-custom {} '(> :bla 2)))) + (is (thrown? java.lang.IllegalArgumentException (parse-custom {} '(> :non-worked 2)))))) + +(deftest test-calc-part-time + (testing "Check whether calculating part time costs works" + ; 4h per day, 10 per month if all hours done, the person did all of thier required hours + (is (= (calc-part-time {:worked 12 :required 24} {:base 10 :per-day 4}) 10.0)) + + ; 4h per day, 10 per month if all hours done, the person is 2 hours low + (is (= (round (calc-part-time {:worked 10 :required 24} {:base 10 :per-day 4})) 8.33)) + + ; 4h per day, 10 per month if all hours done, the person did 10h extra hours + (is (= (round (calc-part-time {:worked 22 :required 24} {:base 10 :per-day 4})) 18.33)) + + ; 8h per day, 10 per month if all hours done, the person did all required hours + (is (= (round (calc-part-time {:worked 24 :required 24} {:base 10 :per-day 8})) 10.0)))) + + +(deftest test-calc-hourly + (testing "Check whether calculating hourly rates works" + (is (= (calc-hourly {:worked 12} {:hourly 10}) 120)) + (is (= (calc-hourly {:worked 12.5} {:hourly 10}) 125.0)) + (is (= (calc-hourly {:worked 12} {:hourly 10.99}) 131.88)))) + + +(deftest test-calc-custom + (testing "Check whether custom formulas work" + ; base per hour + (is (= (calc-custom {:worked 12} {:function '(* :worked 10)}) 120)) + ; Sylwia's formula + (is (= (round (calc-custom {:worked 100 :required 168} + {:function '(+ 1000 (* (- :worked (/ :required 2)) (/ 2000 167)))})) + 1191.62)))) + + +(deftest test-set-price + (let [worked {:worked 100 :required 168}] + (testing "Check the default is to set 0" + (is (= (:netto (set-price worked {})) 0))) + + (testing "Check that :netto is returned if no calc func provided" + (is (= (:netto (set-price worked {:netto 123})) 123))) + + (testing "Check that :per-day is ignored if :base not provided" + (is (= (:netto (set-price worked {:per-day 123})) 0))) + + (testing "Check that part time is calculated if :base provided" + (is (= (round (:netto (set-price worked {:base 100 :per-day 4}))) 119.05))) + + (testing "Check that per hour calculated if :hourly provided" + (is (= (round (:netto (set-price worked {:hourly 10}))) 1000.0))) + + (testing "Check that custom func used if provided" + (is (= (round (:netto (set-price worked {:function '(* :worked 12)}))) 1200.0))))) diff --git a/test/invoices/pdf_test.clj b/test/invoices/pdf_test.clj new file mode 100644 index 0000000..45fb52f --- /dev/null +++ b/test/invoices/pdf_test.clj @@ -0,0 +1,108 @@ +(ns invoices.pdf-test + (:require [clojure.test :refer :all] + [invoices.pdf :refer :all])) + + +(deftest test-format-total + (testing "Check whether the `total` cell gets correctly formatted" + (is (= (format-total [1 2 3 4] identity) [:cell {:background-color [216 247 249]} "10.0"]))) + + (testing "Check whether the `total` cell gets correctly formatted with accessor" + (is (= (format-total [] :netto) [:cell {:background-color [216 247 249]} "0.0"])) + (is (= (format-total [{:netto 12} {:netto 32}] :netto) + [:cell {:background-color [216 247 249]} "44.0"])))) + +(deftest test-format-param + (testing "Check whether parameters get correctly formatted" + (is (= (format-param 123) [:cell.param {:align :right} "123: "])) + (is (= (format-param "bla") [:cell.param {:align :right} "bla: "])))) + +(deftest test-format-value + (testing "Check whether values get correctly formatted" + (is (= (format-value 123) [:cell {:colspan 2} "123"])) + (is (= (format-value "bla") [:cell {:colspan 2} "bla"])))) + +(deftest test-format-product + (testing "Check whether whole products get correctly formatted" + (is (= (format-product {:netto 1000 :vat 23 :title "bla bla"}) + [[:cell {:colspan 4} "bla bla"] "1" "1000.0" "23%" "230.0" "1230.0"])) + (is (= (format-product {:netto 1000 :vat 0 :title "bla bla"}) + [[:cell {:colspan 4} "bla bla"] "1" "1000.0" "0%" "0.0" "1000.0"]))) + + (testing "Check whether no vat is handled correctly" + (is (= (format-product {:netto 1000 :title "bla bla"}) + [[:cell {:colspan 4} "bla bla"] "1" "1000.0" "zw." "0.0" "1000.0"])))) + +(deftest test-get-title + (testing "Check whether getting titles works" + (is (= (get-title nil "mr blobby" "2019/02/11") "mr_blobby_luty_2019_02_11")) + (is (= (get-title "asd" "mr blobby" "2019/02/11") "asd_mr_blobby_luty_2019_02_11")))) + +(deftest test-get-pdf + (testing "Check whether generating pdf bodies works" + (let [seller {:name "Mr. Blobby" + :address "ul. podwodna, 12-345, Mierzów" + :nip 1234567890 + :phone 876543216 + :account "65 2345 1233 1233 4322 3211 4567" + :bank "Skok hop"} + buyer {:name "Buty S.A." + :address "ul. Szewska 32, 76-543, Bąków" + :nip 9875645342} + items [{:vat 8 :netto 123.21 :title "Buty kowbojskie"} + {:netto 321.45 :title "Usługa szewska bez VAT"}] + date (java.time.LocalDate/parse "2018-03-02") + font "/usr/share/fonts/truetype/freefont/FreeSans.ttf"] + (is (= (pdf-body "pdf title" seller buyer items date 12 font) + [{:bottom-margin 10 + :right-margin 50 + :left-margin 10 + :footer "page" + :font "/usr/share/fonts/truetype/freefont/FreeSans.ttf" + :size "a4" + :title "pdf title" + :author "Mr. Blobby" + :top-margin 20} + [:heading "Faktura"] + [:spacer] + [:paragraph "Nr 12"] + [:spacer 2] + [:table {:border false, :padding 0, :spacing 0, :num-cols 6} + [[:cell.param {:align :right} "sprzedawca: "] + [:cell {:colspan 2} "Mr. Blobby"] + [:cell.param {:align :right} "nabywca: "] + [:cell {:colspan 2} "Buty S.A."]] + [[:cell.param {:align :right} "adres: "] + [:cell {:colspan 2} "ul. podwodna, 12-345, Mierzów"] + [:cell.param {:align :right} "adres: "] + [:cell {:colspan 2} "ul. Szewska 32, 76-543, Bąków"]] + [[:cell.param {:align :right} "nip: "] + [:cell {:colspan 2} "1234567890"] + [:cell.param {:align :right} "nip: "] + [:cell {:colspan 2} "9875645342"]] + [[:cell.param {:align :right} "numer telefonu: "] + [:cell {:colspan 2} "876543216"]]] + [:spacer] + [:line] + [:table {:border false, :padding 0, :spacing 0, :num-cols 6} + [[:cell.param {:align :right} "data wystawienia: "] + [:cell {:colspan 2} "2018-03-02"] + [:cell.param {:align :right} "sposób płatności: "] + [:cell {:colspan 2} "Przelew"]] + [[:cell.param {:align :right} "data sprzedaży: "] + [:cell {:colspan 2} "2018-03-02"] + [:cell.param {:align :right} "bank: "] + [:cell {:colspan 2} "Skok hop"]] + [[:cell.param {:align :right} "termin płatności: "] + [:cell {:colspan 2} "2018-03-16"] + [:cell.param {:align :right} "numer konta: "] + [:cell {:colspan 2} "65 2345 1233 1233 4322 3211 4567"]]] + (list :table {:header [{:background-color [216 247 249]} "Lp." [:cell {:colspan 4} "Nazwa"] "Ilość" "Cena netto" "Stawka VAT" "Kwota VAT" "Wartość brutto"], :num-cols 10} + (list 1 [:cell {:colspan 4} "Buty kowbojskie"] "1" "123.21" "8%" "9.86" "133.07") + (list 2 [:cell {:colspan 4} "Usługa szewska bez VAT"] "1" "321.45" "zw." "0.0" "321.45") + [[:cell {:background-color [84 219 229], :colspan 5, :align :center} "Razem"] + [:cell {:background-color [216 247 249]} "2.0"] + [:cell {:background-color [216 247 249]} "444.66"] + "" + [:cell {:background-color [216 247 249]} "9.86"] + [:cell {:background-color [216 247 249]} "454.52"]])]))))) diff --git a/test/invoices/time_test.clj b/test/invoices/time_test.clj new file mode 100644 index 0000000..273891e --- /dev/null +++ b/test/invoices/time_test.clj @@ -0,0 +1,76 @@ +(ns invoices.time-test + (:require [clojure.test :refer :all] + [invoices.time :refer :all])) + + +(deftest test-last-day + (testing "getting last day of month" + (doseq [[day last] [["2019-10-01" "2019-10-31"] + ["2019-06-28" "2019-06-30"] + ["2019-02-03" "2019-02-28"] + ["2020-02-13" "2020-02-29"]]] + (is (= (last-day (java.time.LocalDate/parse day)) (java.time.LocalDate/parse last)))))) + + +(deftest test-prev-month + (testing "getting previous month" + (doseq [[day previous] [["2019-11-01" "2019-10-01"] + ["2019-07-28" "2019-06-01"] + ["2019-03-03" "2019-02-01"] + ["2020-01-13" "2019-12-01"]]] + (is (= (prev-month (java.time.LocalDate/parse day)) (java.time.LocalDate/parse previous)))))) + + +(deftest test-skip-days-off + (testing "Check that work days are returned as is" + (doseq [day ["2019-10-04" ; friday + "2019-10-03" ; thursday + "2019-10-02" + "2019-10-01" + "2019-10-07"]] ; monday + (is (= (skip-days-off (java.time.LocalDate/parse day)) (java.time.LocalDate/parse day))))) + (testing "Check that weekends are skipped" + (doseq [[day friday] [["2019-10-06" "2019-10-04"] ; a sunday + ["2019-10-05" "2019-10-04"] + ["2019-09-01" "2019-08-30"]]] ; month ends are correctly handled + (is (= (skip-days-off (java.time.LocalDate/parse day)) (java.time.LocalDate/parse friday)))))) + + +(deftest test-last-working-day + (testing "Check that getting the last working day works" + (doseq [[day last] [["2019-10-06" "2019-10-31"] ; skip forward to the end of the month + ["2019-08-31" "2019-08-30"]]] ; go back to the last working day + (is (= (last-working-day (java.time.LocalDate/parse day)) (java.time.LocalDate/parse last)))))) + + +(deftest test-date-applies + (testing "Check that all dates apply when no to or from provided" + (doseq [day ["1066-10-06" "2019-10-31" "2219-08-30"]] + (is (date-applies? (java.time.LocalDate/parse day) {:to nil :from nil})))) + + (let [day (java.time.LocalDate/parse "2019-10-10")] + (testing "Check that only dates before :to are valid" + (is (date-applies? day {:to "2020-10-10" :from nil})) + (is (date-applies? day {:to "2019-10-11" :from nil})) + + (is (not (date-applies? day {:to "2019-10-10" :from nil}))) ; the same day is deemed invalid + (is (not (date-applies? day {:to "2010-10-10" :from nil})))) + + (testing "Check that only dates after :from are valid" + (is (date-applies? day {:to nil :from "2000-10-10"})) + (is (date-applies? day {:to nil :from "2019-10-09"})) + (is (date-applies? day {:to nil :from "2019-10-10"})) ; the same day is deemed valid + + (is (not (date-applies? day {:to nil :from "2019-10-11"}))) + (is (not (date-applies? day {:to nil :from "2219-10-11"})))) + + (testing "Check that only dates between :to :from are valid" + (is (date-applies? day {:to "2020-10-10" :from "2000-10-10"})) + (is (date-applies? day {:to "2019-10-11" :from "2019-10-10"})) + + (is (not (date-applies? day {:to "2019-10-10" :from "2019-10-10"}))) + (is (not (date-applies? day {:to "2019-10-11" :from "2019-10-11"})))) + + (testing ":from must be before :to" + (is (not (date-applies? day {:to "2019-10-12" :from "2020-10-10"}))) + (is (not (date-applies? day {:to "2018-10-10" :from "2020-10-10"}))))))