products prices

This commit is contained in:
Daniel O'Connell 2021-03-17 23:06:28 +01:00
parent 1c52af754d
commit c46a505b4c
10 changed files with 147 additions and 40 deletions

View File

@ -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"]}

View File

@ -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)))]

View File

@ -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"

View File

@ -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" {})
])

View File

@ -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

View File

@ -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)]))))

View File

@ -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)))))

View File

@ -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}}))))

View File

@ -1,5 +1,5 @@
[Path]
PathModified=/srv/chickens
PathModified=/srv/chickens/chicken-master.jar
[Install]
WantedBy=multi-user.target

View File

@ -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