From c46a505b4ce32bcee8e09de897e5639569a463d1 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Wed, 17 Mar 2021 23:06:28 +0100 Subject: [PATCH] products prices --- backend/resources/migrations/003-prices.edn | 15 ++++++ backend/src/chicken_master/products.clj | 27 ++++++++--- backend/test/chicken_master/products_test.clj | 46 +++++++++++++------ frontend/src/chicken_master/config.cljs | 5 ++ frontend/src/chicken_master/products.cljs | 3 ++ frontend/src/chicken_master/stock.cljs | 45 +++++++++++------- .../test/chicken_master/products_test.cljs | 17 +++++++ frontend/test/chicken_master/stock_test.cljs | 25 ++++++++++ infra/systemd/chickens-watcher.path | 2 +- infra/systemd/chickens-watcher.service | 2 +- 10 files changed, 147 insertions(+), 40 deletions(-) create mode 100644 backend/resources/migrations/003-prices.edn create mode 100644 frontend/test/chicken_master/stock_test.cljs diff --git a/backend/resources/migrations/003-prices.edn b/backend/resources/migrations/003-prices.edn new file mode 100644 index 0000000..5cff64f --- /dev/null +++ b/backend/resources/migrations/003-prices.edn @@ -0,0 +1,15 @@ +{:up ["CREATE TABLE customer_products ( + id SERIAL, + customer_id INT, + product_id INT, + amount NUMERIC, + price BIGINT, + PRIMARY KEY(id), + CONSTRAINT fk_customer FOREIGN KEY(customer_id) REFERENCES customers(id), + CONSTRAINT fk_product FOREIGN KEY(product_id) REFERENCES products(id) +)" + "ALTER TABLE products ADD price BIGINT" + "ALTER TABLE order_products ADD price BIGINT"] + :down ["ALTER TABLE products DROP COLUMN price" + "ALTER TABLE order_products DROP COLUMN price" + "DROP TABLE customer_products"]} diff --git a/backend/src/chicken_master/products.clj b/backend/src/chicken_master/products.clj index dd50b46..459a9cf 100644 --- a/backend/src/chicken_master/products.clj +++ b/backend/src/chicken_master/products.clj @@ -1,11 +1,12 @@ (ns chicken-master.products (:require [next.jdbc :as jdbc] [next.jdbc.sql :as sql] + [clojure.string :as str] [chicken-master.db :as db])) (defn get-all [user-id] - (->> (sql/query db/db-uri ["SELECT * FROM products WHERE deleted IS NULL AND user_id = ?" user-id]) - (map (fn [{:products/keys [name amount]}] [(keyword name) amount])) + (->> (sql/query db/db-uri ["SELECT name, amount, price FROM products WHERE deleted IS NULL AND user_id = ?" user-id]) + (map (fn [{:products/keys [name amount price]}] [(keyword name) {:amount amount :price price}])) (into {}))) (defn products-map [tx user-id products] @@ -16,13 +17,25 @@ (map #(vector (:products/name %) (:products/id %))) (into {})))) +(defn- update-product [tx user-id prod values] + (let [to-update (seq (filter values [:amount :price])) + cols (->> to-update (map name) (str/join ", ")) + params (concat [(name prod) user-id] (map values to-update)) + updates (->> to-update + (map name) + (map #(str % " = EXCLUDED." %)) + (str/join ", ")) + query (str "INSERT INTO products (name, user_id, " cols ")" + " VALUES" (db/psql-list params) + " ON CONFLICT (name, user_id) DO UPDATE" + " SET deleted = NULL, " updates)] + (when to-update + (jdbc/execute! tx (concat [query] params))))) + (defn update! [user-id new-products] (jdbc/with-transaction [tx db/db-uri] - (doseq [[prod amount] new-products] - (jdbc/execute! tx - ["INSERT INTO products (name, amount, user_id) VALUES(?, ?, ?) - ON CONFLICT (name, user_id) DO UPDATE SET amount = EXCLUDED.amount, deleted = NULL" - (name prod) amount user-id])) + (doseq [[prod values] new-products] + (update-product tx user-id prod values)) (sql/update! tx :products {:deleted true} (into [(str "name NOT IN " (db/psql-list (keys new-products)))] diff --git a/backend/test/chicken_master/products_test.clj b/backend/test/chicken_master/products_test.clj index 79bf722..fb4f713 100644 --- a/backend/test/chicken_master/products_test.clj +++ b/backend/test/chicken_master/products_test.clj @@ -8,14 +8,14 @@ (deftest test-get-all (testing "query is correct" (with-redefs [sql/query (fn [_ query] - (is (= query ["SELECT * FROM products WHERE deleted IS NULL AND user_id = ?" "1"])) + (is (= query ["SELECT name, amount, price FROM products WHERE deleted IS NULL AND user_id = ?" "1"])) [])] (sut/get-all "1"))) (testing "correct format" - (with-redefs [sql/query (constantly [{:products/name "eggs" :products/amount 12} - {:products/name "milk" :products/amount 3}])] - (is (= (sut/get-all "1") {:eggs 12 :milk 3}))))) + (with-redefs [sql/query (constantly [{:products/name "eggs" :products/amount 12 :products/price nil} + {:products/name "milk" :products/amount 3 :products/price 12}])] + (is (= (sut/get-all "1") {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 12}}))))) (deftest test-products-map (testing "no products" @@ -45,16 +45,33 @@ (deftest test-update! (testing "each item gets updated" (let [inserts (atom []) - update-query "INSERT INTO products (name, amount, user_id) VALUES(?, ?, ?)\n ON CONFLICT (name, user_id) DO UPDATE SET amount = EXCLUDED.amount, deleted = NULL"] + update-query (str "INSERT INTO products (name, user_id, amount, price) VALUES(?, ?, ?, ?) " + "ON CONFLICT (name, user_id) " + "DO UPDATE SET deleted = NULL, amount = EXCLUDED.amount, price = EXCLUDED.price")] (with-redefs [jdbc/transact (fn [_ f & args] (apply f args)) jdbc/execute! #(swap! inserts conj %2) sql/update! (constantly nil) sql/query (constantly [])] - (sut/update! :user-id {:eggs 2 :milk 3 :cows 2}) - (is (= (sort @inserts) - [[update-query "cows" 2 :user-id] - [update-query "eggs" 2 :user-id] - [update-query "milk" 3 :user-id]]))))) + (sut/update! :user-id {:eggs {:amount 2 :price 1} :milk {:amount 3 :price 2} :cows {:amount 2 :price 3}}) + (is (= (sort-by second @inserts) + [[update-query "cows" :user-id 2 3] + [update-query "eggs" :user-id 2 1] + [update-query "milk" :user-id 3 2]]))))) + + (testing "missing fields are ignored" + (let [inserts (atom [])] + (with-redefs [jdbc/transact (fn [_ f & args] (apply f args)) + jdbc/execute! #(swap! inserts conj %2) + sql/update! (constantly nil) + sql/query (constantly [])] + (sut/update! :user-id {:eggs {:amount 2} :milk {:amount 3} :cows {}}) + (is (= (sort-by second @inserts) + [[(str "INSERT INTO products (name, user_id, amount) VALUES(?, ?, ?) " + "ON CONFLICT (name, user_id) DO UPDATE " + "SET deleted = NULL, amount = EXCLUDED.amount") "eggs" :user-id 2] + [(str "INSERT INTO products (name, user_id, amount) VALUES(?, ?, ?) " + "ON CONFLICT (name, user_id) DO UPDATE SET " + "deleted = NULL, amount = EXCLUDED.amount") "milk" :user-id 3]]))))) (testing "non selected items get removed" (let [updates (atom [])] @@ -62,16 +79,17 @@ jdbc/execute! (constantly nil) sql/update! (partial swap! updates conj) sql/query (constantly [])] - (sut/update! :user-id {:eggs 2 :milk 3 :cows 2}) + (sut/update! :user-id {:eggs {:amount 2} :milk {:amount 3} :cows {:amount 2}}) (is (= @updates [{} :products {:deleted true} ["name NOT IN (?, ?, ?)" "eggs" "milk" "cows"]]))))) (testing "non selected items get removed" (with-redefs [jdbc/transact (fn [_ f & args] (apply f args)) jdbc/execute! (constantly nil) sql/update! (constantly nil) - sql/query (constantly [{:products/name "eggs" :products/amount 12} - {:products/name "milk" :products/amount 3}])] - (is (= (sut/update! :user-id {:eggs 2 :milk 3 :cows 2}) {:eggs 12 :milk 3}))))) + sql/query (constantly [{:products/name "eggs" :products/amount 12 :products/price 1} + {:products/name "milk" :products/amount 3 :products/price 2}])] + (is (= (sut/update! :user-id {:eggs {:amount 2} :milk {:amount 3} :cows {:amount 2}}) + {:eggs {:amount 12 :price 1} :milk {:amount 3 :price 2}}))))) (deftest update-products-mapping-test (testing "items get removed" diff --git a/frontend/src/chicken_master/config.cljs b/frontend/src/chicken_master/config.cljs index 21fee24..7ab0cc6 100644 --- a/frontend/src/chicken_master/config.cljs +++ b/frontend/src/chicken_master/config.cljs @@ -32,6 +32,8 @@ :editable-number-inputs (get-setting :editable-number-inputs false) ; only allow number modifications in the edit modal :hide-fulfilled-orders (get-setting :hide-fulfilled-orders false) + :prices (get-setting :prices true) + :backend-url (get-setting :backend-url (if (= (.. js/window -location -href) "http://localhost:8280/") "http://localhost:3000/api/" @@ -93,6 +95,9 @@ (input :editable-number-inputs "możliwość bezposredniej edycji" {:type :checkbox}) (input :hide-fulfilled-orders "ukryj wydane zamówienia" {:type :checkbox}) + [:h3 "Ustawienia magazynu"] + (input :prices "pokaż ceny" {:type :checkbox}) + [:h3 "Ustawienia tyłu"] (input :backend-url "backend URL" {}) ]) diff --git a/frontend/src/chicken_master/products.cljs b/frontend/src/chicken_master/products.cljs index e18ee39..33d7326 100644 --- a/frontend/src/chicken_master/products.cljs +++ b/frontend/src/chicken_master/products.cljs @@ -14,6 +14,9 @@ (let [div (js/Math.pow 10 digits)] (/ (js/Math.round (* num div)) div))) +(defn format-price [price] (when price (round (/ price 100) 2))) +(defn normalise-price [price] (when price (round (* price 100) 0))) + (defn number-input [id label amount on-blur] (html/input id label {:type :number diff --git a/frontend/src/chicken_master/stock.cljs b/frontend/src/chicken_master/stock.cljs index c78109d..2435b8e 100644 --- a/frontend/src/chicken_master/stock.cljs +++ b/frontend/src/chicken_master/stock.cljs @@ -1,7 +1,9 @@ (ns chicken-master.stock (:require + [clojure.string :as str] [re-frame.core :as re-frame] [reagent.core :as reagent] + [chicken-master.config :as config] [chicken-master.products :as prod] [chicken-master.subs :as subs] [chicken-master.html :as html] @@ -11,17 +13,33 @@ (let [state (reagent/atom stock)] (fn [] [:div - (for [[product amount] @state] - [:div {:key (gensym) :class :stock-product} - [:span {:class :product-name} product] - [:div {:class :stock-product-amount} - [:button {:type :button :on-click #(swap! state update product dec)} "-"] - (prod/number-input (name product) "" (or amount 0) - #(swap! state assoc product (-> % .-target .-value prod/num-or-nil))) - [:button {:type :button :on-click #(swap! state update product inc)} "+"] - [:button {:type :button :on-click #(swap! state dissoc product)} "x"]]]) + (doall + (for [[product {:keys [amount price]}] @state] + [:div {:key (gensym) :class :stock-product} + [:span {:class :product-name} product] + [:div {:class :stock-product-amount} + [:button {:type :button :on-click #(swap! state update-in [product :amount] dec)} "-"] + (prod/number-input (str (name product) "-amount") "" (or amount 0) + #(swap! state assoc-in [product :amount] (-> % .-target .-value prod/num-or-nil))) + [:button {:type :button :on-click #(swap! state update-in [product :amount] inc)} "+"] + [:button {:type :button :on-click #(swap! state dissoc product)} "x"]] + (when (config/settings :prices) + [:div {:class :stock-product-price} + (prod/number-input (str (name product) "-price") "cena" (prod/format-price price) + #(swap! state assoc-in + [product :price] + (some-> % .-target .-value prod/num-or-nil prod/normalise-price)))])])) [prod/item-adder :callback #(swap! state assoc (keyword %) 0) :button "+"]]))) +(defn process-form [form] + (->> form + (filter (comp prod/num-or-nil second)) + (map (fn [[k v]] [(str/split k #"-") (prod/num-or-nil v)])) + (group-by ffirst) + (map (fn [[k vals]] [(keyword k) (reduce #(assoc %1 (-> %2 first second keyword) (second %2)) {} vals)])) + (map (fn [[k vals]] [k (update vals :price prod/normalise-price)])) + (into {}))) + (defn show-available [] (html/modal :stock @@ -29,11 +47,4 @@ [:h2 "Magazyn"] [stock-form @(re-frame/subscribe [::subs/available-products])]] ;; On success - :on-submit (fn [form] - (->> form - (reduce-kv #(if-let [val (prod/num-or-nil %3)] - (assoc %1 (keyword %2) val) - %1) - {}) - (conj [::event/save-stock]) - re-frame/dispatch)))) + :on-submit (fn [form] (re-frame/dispatch [::event/save-stock (process-form form)])))) diff --git a/frontend/test/chicken_master/products_test.cljs b/frontend/test/chicken_master/products_test.cljs index c975f48..120321e 100644 --- a/frontend/test/chicken_master/products_test.cljs +++ b/frontend/test/chicken_master/products_test.cljs @@ -27,3 +27,20 @@ (is (= (sut/round 1.234567 1) 1.2)) (is (= (sut/round 1.234567 2) 1.23)) (is (= (sut/round 1.234567 3) 1.235)))) + +(deftest test-prices + (testing "prices are formatted" + (is (= (sut/format-price 0) 0)) + (is (= (sut/format-price 10) 0.1)) + (is (= (sut/format-price 1234567890) 12345678.9))) + + (testing "prices get normalised" + (is (= (sut/normalise-price 0) 0)) + (is (= (sut/normalise-price 10) 1000)) + (is (= (sut/normalise-price 12.34) 1234)) + (is (= (sut/normalise-price 12.345678) 1235)) + (is (= (sut/normalise-price 12.325678) 1233))) + + (testing "nil prices are handled" + (is (nil? (sut/format-price nil))) + (is (nil? (sut/normalise-price nil))))) diff --git a/frontend/test/chicken_master/stock_test.cljs b/frontend/test/chicken_master/stock_test.cljs new file mode 100644 index 0000000..f8fa319 --- /dev/null +++ b/frontend/test/chicken_master/stock_test.cljs @@ -0,0 +1,25 @@ +(ns chicken-master.stock-test + (:require + [chicken-master.stock :as sut] + [cljs.test :refer-macros [deftest is testing]])) + + +(deftest process-form-test + (testing "no values" + (is (= (sut/process-form {}) {}))) + + (testing "non numeric values are removed" + (is (= (sut/process-form {"bla" "dew"}) {}))) + + (testing "price and amount are extracted" + (is (= (sut/process-form {"bla" "dew" "ble-amount" "123" "ble-price" "4.32"}) + {:ble {:amount 123 :price 432}}))) + + (testing "multiple values are handled" + (is (= (sut/process-form {"cheese-price" "0.12" "user-name" "" "carrots-amount" "-1" + "eggs-amount" "8" "cows-amount" "15" "carrots-price" "31.3" + "eggs-price" "0" "cows-price" "0" "cheese-amount" "4"}) + {:cheese {:price 12, :amount 4} + :carrots {:amount -1, :price 3130} + :eggs {:amount 8, :price 0} + :cows {:amount 15, :price 0}})))) diff --git a/infra/systemd/chickens-watcher.path b/infra/systemd/chickens-watcher.path index 186eba7..b9dca89 100644 --- a/infra/systemd/chickens-watcher.path +++ b/infra/systemd/chickens-watcher.path @@ -1,5 +1,5 @@ [Path] -PathModified=/srv/chickens +PathModified=/srv/chickens/chicken-master.jar [Install] WantedBy=multi-user.target \ No newline at end of file diff --git a/infra/systemd/chickens-watcher.service b/infra/systemd/chickens-watcher.service index 32ab923..04c08f4 100644 --- a/infra/systemd/chickens-watcher.service +++ b/infra/systemd/chickens-watcher.service @@ -4,7 +4,7 @@ After=postgres.service [Service] Type=oneshot -ExecStart=/usr/bin/systemctl restart chickens.service +ExecStart=/bin/systemctl restart chickens.service [Install] WantedBy=multi-user.target \ No newline at end of file