From dfccbf35cd64aadf6a77bb8a0d396bbd74485294 Mon Sep 17 00:00:00 2001 From: Bryce Date: Wed, 3 Apr 2024 20:08:46 -0700 Subject: [PATCH] Makes editing work correctly for non-admins --- src/clj/auto_ap/ssr/components/aside.clj | 153 ++++++++++-------- .../auto_ap/ssr/components/multi_modal.clj | 3 +- .../ssr/invoice/new_invoice_wizard.clj | 122 +++++++++----- src/clj/auto_ap/ssr/invoices.clj | 14 +- src/clj/user.clj | 51 +++--- src/cljc/auto_ap/permissions.cljc | 43 +++++ .../ssr/invoice/new_invoice_wizard_test.clj | 94 +++++++++++ 7 files changed, 341 insertions(+), 139 deletions(-) create mode 100644 test/clj/auto_ap/ssr/invoice/new_invoice_wizard_test.clj diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index e3a97791..cf69ba58 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -1,13 +1,13 @@ (ns auto-ap.ssr.components.aside (:require [auto-ap.client-routes :as client-routes] - [auto-ap.logging :as alog] + [auto-ap.permissions :refer [can?]] [auto-ap.routes.admin.clients :as ac-routes] [auto-ap.routes.admin.excel-invoices :as ei-routes] [auto-ap.routes.admin.import-batch :as ib-routes] - [auto-ap.routes.outgoing-invoice :as oi-routes] [auto-ap.routes.admin.transaction-rules :as transaction-rules] [auto-ap.routes.admin.vendors :as v-routes] [auto-ap.routes.invoice :as invoice-route] + [auto-ap.routes.outgoing-invoice :as oi-routes] [auto-ap.routes.payments :as payment-routes] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.hiccup-helper :as hh] @@ -131,53 +131,56 @@ :active? (= ::invoice-route/voided-page (:matched-route request)) :hx-boost "true"} "Voided") - (menu-button- {:href (bidi/path-for ssr-routes/only-routes - ::oi-routes/new) - :active? (= ::oi-routes/new (:matched-route request)) - :hx-boost "true"} - "Create outgoing")) + (menu-button- {:href (bidi/path-for client-routes/routes + :import-invoices)} "Import") + #_(menu-button- {:href (bidi/path-for ssr-routes/only-routes + ::oi-routes/new) + :active? (= ::oi-routes/new (:matched-route request)) + :hx-boost "true"} + "Create outgoing")) - (menu-button- {:icon svg/receipt-register-1 + (when + (can? (:identity request) {:subject :sales :activity :read}) + (list + (menu-button- {:icon svg/receipt-register-1 - "@click.prevent" "if (selected == 'sales') {selected = null } else { selected = 'sales'} "} - "Sales") - (sub-menu- {:selector "sales" - :active? (= "sales" selected)} - (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes - :pos-sales) - "?date-range=week") - :active? (= :pos-sales (:matched-route request)) - :hx-boost "true"} - "Sales") - (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes - :pos-expected-deposits) - "?date-range=week") - :active? (= :pos-expected-deposits (:matched-route request)) - :hx-boost "true"} - "Expected Deposits") - (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes - :pos-tenders) - "?date-range=week") - :active? (= :pos-tenders (:matched-route request)) - :hx-boost "true"} - "Tenders") + "@click.prevent" "if (selected == 'sales') {selected = null } else { selected = 'sales'} "} + "Sales") + (sub-menu- {:selector "sales" + :active? (= "sales" selected)} + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-sales) + "?date-range=week") + :active? (= :pos-sales (:matched-route request)) + :hx-boost "true"} + "Sales") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-expected-deposits) + "?date-range=week") + :active? (= :pos-expected-deposits (:matched-route request)) + :hx-boost "true"} + "Expected Deposits") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-tenders) + "?date-range=week") + :active? (= :pos-tenders (:matched-route request)) + :hx-boost "true"} + "Tenders") - (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes - :pos-refunds) - "?date-range=week") - :active? (= :pos-refunds (:matched-route request)) - :hx-boost "true"} + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-refunds) + "?date-range=week") + :active? (= :pos-refunds (:matched-route request)) + :hx-boost "true"} + + "Refunds") + (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes + :pos-cash-drawer-shifts) + "?date-range=week") + :active? (= :cash-drawer-shifts (:matched-route request)) + :hx-boost "true"} + "Cash drawer shifts")))) - "Refunds") - (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes - :pos-cash-drawer-shifts) - "?date-range=week") - :active? (= :cash-drawer-shifts (:matched-route request)) - :hx-boost "true"} - "Cash drawer shifts")) - ;; TODO make specific routes for categories - ;; TODO auto-apen sub menus - (menu-button- {"@click.prevent" "if (selected == 'payments') {selected = null } else { selected = 'payments'} " :icon svg/payments} "Payments") @@ -224,26 +227,35 @@ :requires-feedback-transactions)} "Client Review") (menu-button- {:href (bidi/path-for client-routes/routes :approved-transactions)} "Approved") - (menu-button- {:href (bidi/path-for ssr-routes/only-routes - :transaction-insights)} "Insights"))] + (when (can? (:identity request) + {:subject :transaction :activity :insights}) + (menu-button- {:href (bidi/path-for ssr-routes/only-routes + :transaction-insights)} "Insights")))] - (menu-button- {"@click.prevent" "if (selected == 'ledger') {selected = null } else { selected = 'ledger'} " - :icon svg/receipt} - "Ledger") - (sub-menu- {:selector "ledger" - :active? (= "ledger" selected)} - (menu-button- {:href (bidi/path-for client-routes/routes - :ledger)} "Register") - (menu-button- {:href (bidi/path-for client-routes/routes - :profit-and-loss)} "Profit & Loss") - (menu-button- {:href (bidi/path-for client-routes/routes - :profit-and-loss-detail)} "Profit & Loss Detail") - (menu-button- {:href (bidi/path-for client-routes/routes - :cash-flows)} "Cash Flows") - (menu-button- {:href (bidi/path-for client-routes/routes - :balance-sheet)} "Balance Sheet") - (menu-button- {:href (bidi/path-for client-routes/routes - :external-import-ledger)} "External Ledger Import"))])) + + (when (can? (:identity request) + {:subject :ledger-page}) + (list + (menu-button- {"@click.prevent" "if (selected == 'ledger') {selected = null } else { selected = 'ledger'} " + :icon svg/receipt} + "Ledger") + (sub-menu- {:selector "ledger" + :active? (= "ledger" selected)} + (menu-button- {:href (bidi/path-for client-routes/routes + :ledger)} "Register") + (menu-button- {:href (bidi/path-for client-routes/routes + :profit-and-loss)} "Profit & Loss") + (menu-button- {:href (bidi/path-for client-routes/routes + :profit-and-loss-detail)} "Profit & Loss Detail") + (menu-button- {:href (bidi/path-for client-routes/routes + :cash-flows)} "Cash Flows") + (menu-button- {:href (bidi/path-for client-routes/routes + :balance-sheet)} "Balance Sheet") + (when (can? (:identity request) + {:subject :ledger + :activity :import}) + (menu-button- {:href (bidi/path-for client-routes/routes + :external-import-ledger)} "External Ledger Import")))))])) (defn company-aside-nav- [_] @@ -330,7 +342,7 @@ :active? (= :admin-rules matched-route) :href (bidi/path-for ssr-routes/only-routes :admin-history) - :hx-boost "true" } + :hx-boost "true"} "History")] [:li @@ -342,9 +354,10 @@ "Background Jobs")] - (menu-button- {:icon svg/arrow-in - "@click.prevent" "if (selected == 'import') {selected = null } else { selected = 'import'} "} - "Import") + (when (can? (:identity request) {:subject :invoice :activity :import}) + (menu-button- {:icon svg/arrow-in + "@click.prevent" "if (selected == 'import') {selected = null } else { selected = 'import'} "} + "Import")) (sub-menu- {:selector "import"} (menu-button- {:href (bidi/path-for ssr-routes/only-routes @@ -356,10 +369,10 @@ (menu-button- {:href (bidi/path-for ssr-routes/only-routes ::ib-routes/page) :active? (= ::ib-routes/page matched-route) - :hx-boost true} + :hx-boost true} "Import Batches") (menu-button- {:href (bidi/path-for ssr-routes/only-routes :admin-ezcater-xls) :active? (= :admin-ezcater-xls matched-route) - :hx-boost "true"} + :hx-boost "true"} "EZCater XLS Import"))]) diff --git a/src/clj/auto_ap/ssr/components/multi_modal.clj b/src/clj/auto_ap/ssr/components/multi_modal.clj index 6b1c4181..74c664c5 100644 --- a/src/clj/auto_ap/ssr/components/multi_modal.clj +++ b/src/clj/auto_ap/ssr/components/multi_modal.clj @@ -20,8 +20,7 @@ :hx-swap "outerHTML" :hx-target-400 "#form-errors .error-content" :hx-trigger "submit" - :hx-target "this" - "x-trap" "true"}) + :hx-target "this" }) (defprotocol ModalWizardStep (step-key [this]) diff --git a/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj b/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj index 383200b6..cd8aa987 100644 --- a/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj +++ b/src/clj/auto_ap/ssr/invoice/new_invoice_wizard.clj @@ -5,6 +5,7 @@ [auto-ap.datomic.invoices :as d-invoices] [auto-ap.graphql.utils :refer [assert-can-see-client assert-not-locked exception->4xx]] + [auto-ap.logging :as alog] [auto-ap.routes.invoice :as route] [auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]] @@ -69,8 +70,6 @@ (defn check-vendor-default-account [vendor-id] (some? (:vendor/default-account (get-vendor vendor-id)))) -;; TODO negative expense accounts for negative invoices? - (def new-form-schema [:map [:db/id {:optional true} [:maybe entity-id]] @@ -97,7 +96,7 @@ [:fn {:error/message "Not an allowed account."} check-allowance]]] [:invoice-expense-account/location :string] - [:invoice-expense-account/amount [:double {:min 0}]]] + [:invoice-expense-account/amount :double]] [:fn {:error/fn (fn [r x] (:type r)) :error/path [:invoice-expense-account/location]} check-invoice-expense-account-location]]]]]) @@ -557,36 +556,67 @@ (when-not (dollars= total expense-account-total) (form-validation-error (str "Expense account total (" expense-account-total ") does not equal invoice total (" total ")"))))) -(defn maybe-spread-locations [invoice] - (let [valid-locations (pull-attr (dc/db conn) :client/locations (:invoice/client invoice))] - (with-precision 2 - (let [expense-accounts (vec (mapcat - (fn [ea] - (let [cents-to-distribute (int (Math/round (Math/abs (* (:invoice-expense-account/amount ea) 100))))] - (if (= "Shared" (:invoice-expense-account/location ea)) - (->> valid-locations - (map - (fn [cents location] - (assoc ea - :db/id (random-tempid) - :invoice-expense-account/amount (* 0.01 cents) - :invoice-expense-account/location location)) - (rm/spread-cents cents-to-distribute (count valid-locations)))) - [ea]))) - (:invoice/expense-accounts invoice))) - expense-accounts (mapv - (fn [a] - (update a :invoice-expense-account/amount - #(with-precision 2 - (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP))))) - expense-accounts) - leftover (with-precision 2 (.round (bigdec (- (Math/abs (:invoice/total invoice)) - (Math/abs (reduce + 0.0 (map #(:invoice-expense-account/amount %) expense-accounts))))) - *math-context*)) - accounts (if (seq expense-accounts) - (update-in expense-accounts [(dec (count expense-accounts)) :invoice-expense-account/amount] #(+ % (double leftover))) - [])] - (assoc invoice :invoice/expense-accounts (into [] accounts)))))) + +(defn- calculate-spread + "Helper function to calculate the amount to be assigned to each location" + [shared-amount total-locations] + (let [base-amount (int (/ shared-amount total-locations)) + remainder (- shared-amount (* base-amount total-locations))] + {:base-amount base-amount + :remainder remainder})) + + +(defn- spread-expense-account + "Spreads the expense account amount across the given locations" + [locations expense-account] + (if (= "Shared" (:invoice-expense-account/location expense-account)) + (let [{:keys [base-amount remainder]} (calculate-spread (:invoice-expense-account/amount expense-account) (count locations))] + (map-indexed (fn [idx _] + (assoc expense-account + :invoice-expense-account/amount (+ base-amount (if (< idx remainder) 1 0)) + :invoice-expense-account/location (nth locations idx))) + locations)) + [expense-account])) + +(defn $->cents [x] + (int + (let [result (* 100M (bigdec x))] + (.setScale result 0 java.math.BigDecimal/ROUND_HALF_UP)))) + +(defn cents->$ [x] + (double + (let [result (* 0.01M (bigdec x))] + (.setScale result 2 java.math.BigDecimal/ROUND_HALF_UP)))) + +(defn- apply-total-delta-to-account [invoice-total eas] + (when (seq eas) + (let [leftover (- invoice-total (reduce + 0 (map :invoice-expense-account/amount eas))) + leftover-beyond-a-single-cent? (or (< leftover -1) + (> leftover 1)) + leftover (if leftover-beyond-a-single-cent? + 0 + leftover) + [first-eas & rest] eas] + (cons + (update first-eas :invoice-expense-account/amount #(+ % leftover)) + rest)))) + + +(defn maybe-spread-locations + "Converts any expense account for a \"Shared\" location into a separate expense account for all valid locations for that client" + ([invoice] + (maybe-spread-locations invoice (pull-attr (dc/db conn) :client/locations (:invoice/client invoice)))) + ([invoice locations] + (update-in invoice + [:invoice/expense-accounts] + (fn [expense-accounts] + (->> expense-accounts + (map (fn [ea] (update ea :invoice-expense-account/amount $->cents))) + (mapcat (partial spread-expense-account locations)) + (apply-total-delta-to-account ($->cents (:invoice/total invoice))) + (map (fn [ea] (update ea :invoice-expense-account/amount cents->$)))))))) + + (defrecord NewWizard2 [_ current-step] mm/LinearModalWizard @@ -622,9 +652,27 @@ new-form-schema) (submit [this {:keys [multi-form-state request-method identity] :as request}] (let [invoice (:snapshot multi-form-state) + + _ (alog/peek invoice) extant? (:db/id invoice) client-id (->db-id (:invoice/client invoice)) vendor-id (->db-id (:invoice/vendor invoice)) + paid-amount (if-let [outstanding-balance + (and extant? + (- + (pull-attr (dc/db conn) + :invoice/total + (:db/id invoice)) + (pull-attr (dc/db conn) + :invoice/outstanding-balance + (:db/id invoice))))] + outstanding-balance + 0.0) + outstanding-balance (- (or + (:invoice/total (:step-params multi-form-state)) + (:invoice/total (:snapshot multi-form-state))) + paid-amount) + transaction [:upsert-invoice (-> multi-form-state :snapshot (assoc :db/id (or (:db/id invoice) "invoice")) @@ -638,11 +686,11 @@ :invoice-expense-account/amount (or (:invoice/total (:step-params multi-form-state)) (:invoice/total (:snapshot multi-form-state)))}])) (assoc - :invoice/outstanding-balance (or - (:invoice/total (:step-params multi-form-state)) - (:invoice/total (:snapshot multi-form-state))) + :invoice/outstanding-balance outstanding-balance :invoice/import-status :import-status/imported - :invoice/status :invoice-status/unpaid) + :invoice/status (if (dollars= 0.0 outstanding-balance) + :invoice-status/paid + :invoice-status/unpaid)) (maybe-spread-locations) (update :invoice/date coerce/to-date) (update :invoice/due coerce/to-date) diff --git a/src/clj/auto_ap/ssr/invoices.clj b/src/clj/auto_ap/ssr/invoices.clj index 9dfc9617..7cc532b5 100644 --- a/src/clj/auto_ap/ssr/invoices.clj +++ b/src/clj/auto_ap/ssr/invoices.clj @@ -401,17 +401,19 @@ (when (can? (:identity request) {:subject :invoice :activity :create}) (com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)} "New invoice"))]) - :row-buttons (fn [_ entity] - [(when (= :invoice-status/unpaid (:invoice/status entity)) + :row-buttons (fn [request entity] + [(when (and (= :invoice-status/unpaid (:invoice/status entity)) + (can? (:identity request) {:subject :invoice :activity :delete})) (com/icon-button {:hx-delete (bidi/path-for ssr-routes/only-routes ::route/delete :db/id (:db/id entity)) :hx-confirm "Are you sure you want to void this invoice?"} svg/trash)) - (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes - ::route/edit-wizard - :db/id (:db/id entity)) } - svg/pencil)]) + (when (can? (:identity request) {:subject :invoice :activity :edit}) + (com/icon-button {:hx-put (bidi/path-for ssr-routes/only-routes + ::route/edit-wizard + :db/id (:db/id entity))} + svg/pencil))]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes ::route/page)} "Invoices"]] :title (fn [r] diff --git a/src/clj/user.clj b/src/clj/user.clj index 0e3e2818..0ce1ba08 100644 --- a/src/clj/user.clj +++ b/src/clj/user.clj @@ -19,6 +19,7 @@ [com.brunobonacci.mulog.buffer :as rb] [config.core :refer [env]] [datomic.api :as dc] + [puget.printer :as puget] [datomic.api :as d] [figwheel.main.api] [hawk.core] @@ -27,30 +28,32 @@ (:import (org.apache.commons.io.input BOMInputStream) [org.eclipse.jetty.server.handler.gzip GzipHandler])) - (defn println-event [item] - (printf "%s: %s - %s:%s by %s\n" - (str (c/to-date-time (:mulog/timestamp item))) - (:mulog/namespace item) (:mulog/event-name item) - (if (:mulog/duration item) - (str " " (int (/ (:mulog/duration item) 1000000)) "ms") - "") - (:user-name item)) - (println (reduce - (fn [acc [k v]] - (assoc acc k v)) - {} - (dissoc - item - :user))) - #_(puget/cprint (reduce - (fn [acc [k v]] - (assoc acc k v)) - {} - (dissoc - item - :user)) - {:seq-limit 10}) + #_(printf "%s: %s - %s:%s by %s\n" + (str (c/to-date-time (:mulog/timestamp item))) + (:mulog/namespace item) (:mulog/event-name item) + (if (:mulog/duration item) + (str " " (int (/ (:mulog/duration item) 1000000)) "ms") + "") + (:user-name item)) + #_(println (reduce + (fn [acc [k v]] + (assoc acc k v)) + {} + (dissoc + item + :user))) + (when (= :auto-ap.logging/peek (:mulog/event-name item)) + (println "\u001B[31mTEST") + ) + (puget/cprint (reduce + (fn [acc [k v]] + (assoc acc k v)) + {} + (dissoc + item + :user)) + {:seq-limit 10}) (println)) @@ -354,7 +357,7 @@ `src` or `resources`." [] (println "starting auto reset") - (hawk.core/watch! [{:paths ["src/"] + (hawk.core/watch! [{:paths ["src/" "test/"] :handler auto-reset-handler}])) diff --git a/src/cljc/auto_ap/permissions.cljc b/src/cljc/auto_ap/permissions.cljc index ee183252..d1287f85 100644 --- a/src/cljc/auto_ap/permissions.cljc +++ b/src/cljc/auto_ap/permissions.cljc @@ -25,6 +25,24 @@ (#{:invoice-page :payment-page :my-company-page :transaction-page :ledger-page} subject) true + (= [:invoice :import] [subject activity]) + true + + (= [:invoice :create] [subject activity]) + true + + (= [:invoice :pay] [subject activity]) + true + + (= [:invoice :edit] [subject activity]) + true + + (= [:invoice :delete] [subject activity]) + true + + (= [:sales :read] [subject activity]) + true + (= [:vendor :create] [subject activity]) true @@ -44,6 +62,18 @@ (= [:vendor :edit] [subject activity]) true + (= [:invoice :create] [subject activity]) + true + + (= [:invoice :pay] [subject activity]) + true + + (= [:invoice :edit] [subject activity]) + true + + (= [:invoice :delete] [subject activity]) + true + :else false) (#{:user-role/read-only "read-only"} role) @@ -66,6 +96,19 @@ (= [:signature :edit] [subject activity]) true + + (= [:invoice :create] [subject activity]) + true + + (= [:invoice :pay] [subject activity]) + true + + (= [:invoice :edit] [subject activity]) + true + + (= [:invoice :delete] [subject activity]) + true + :else false) :else diff --git a/test/clj/auto_ap/ssr/invoice/new_invoice_wizard_test.clj b/test/clj/auto_ap/ssr/invoice/new_invoice_wizard_test.clj new file mode 100644 index 00000000..b71e88c0 --- /dev/null +++ b/test/clj/auto_ap/ssr/invoice/new_invoice_wizard_test.clj @@ -0,0 +1,94 @@ +(ns auto-ap.ssr.invoice.new-invoice-wizard-test + (:require [clojure.test :refer [deftest testing is]] + [auto-ap.ssr.invoice.new-invoice-wizard :as sut9])) + + +(deftest maybe-spread-locations-test + (testing "Shared amount correctly spread across multiple locations" + (let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount 100.0 + :invoice-expense-account/location "Shared"}] + :invoice/total 100.0} + result (sut9/maybe-spread-locations invoice ["Location 1" + "Location 2"])] + (is (= + [{:invoice-expense-account/amount 50.0 + :invoice-expense-account/location "Location 1"} + {:invoice-expense-account/amount 50.0 + :invoice-expense-account/location "Location 2"}] + (map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result)))))) + + (testing "Shared amount correctly spread with negative amounts" + (let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount -100.0 + :invoice-expense-account/location "Shared"}] + :invoice/total -100.0} + result (sut9/maybe-spread-locations invoice ["Location 1" + "Location 2"])] + (is (= + [{:invoice-expense-account/amount -50.0 + :invoice-expense-account/location "Location 1"} + {:invoice-expense-account/amount -50.0 + :invoice-expense-account/location "Location 2"}] + (map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result)))))) + + + + (testing "Shared amount correctly spread with leftovers" + (let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount 100.0 + :invoice-expense-account/location "Shared"}] + :invoice/total 100.0} + result (sut9/maybe-spread-locations invoice ["Location 1" + "Location 2" + "Location 3"])] + (is (= + [{:invoice-expense-account/amount 33.34 + :invoice-expense-account/location "Location 1"} + {:invoice-expense-account/amount 33.33 + :invoice-expense-account/location "Location 2"} + {:invoice-expense-account/amount 33.33 + :invoice-expense-account/location "Location 3"}] + (map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result)))))) + + (testing "Shared amount correctly spread with leftovers in negatives" + (let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount -33.33333333333333 + :invoice-expense-account/location "A"} + {:invoice-expense-account/amount -33.33333333333333 + :invoice-expense-account/location "B"} + {:invoice-expense-account/amount -33.33333333333333 + :invoice-expense-account/location "C"}] + :invoice/total -100.0} + result (sut9/maybe-spread-locations invoice ["Location 1"])] + (is (= [{:invoice-expense-account/amount -33.34 + :invoice-expense-account/location "A"} + {:invoice-expense-account/amount -33.33 + :invoice-expense-account/location "B"} + {:invoice-expense-account/amount -33.33 + :invoice-expense-account/location "C"}] + (map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result)))))) + + (testing "Shared amount correctly spread with negative amounts and leftovers" + (let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount -101.33 + :invoice-expense-account/location "Shared"}] + :invoice/total -101.33} + result (sut9/maybe-spread-locations invoice ["Location 1" + "Location 2"])] + (is (= + [{:invoice-expense-account/amount -50.67 + :invoice-expense-account/location "Location 1"} + {:invoice-expense-account/amount -50.66 + :invoice-expense-account/location "Location 2"}] + (map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result)))))) + + (testing "Leftovers should not exceed a single cent" + (let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount -100 + :invoice-expense-account/location "Shared"} + {:invoice-expense-account/amount -5 + :invoice-expense-account/location "Shared"}] + :invoice/total -101} + result (sut8/maybe-spread-locations invoice ["Location 1" ])] + (is (= + [{:invoice-expense-account/amount -100.0 + :invoice-expense-account/location "Location 1"} + {:invoice-expense-account/amount -5.0 + :invoice-expense-account/location "Location 1"}] + (map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result))))))) +