mirror of
https://github.com/mruwnik/chicken-master.git
synced 2025-06-08 21:34:43 +02:00
products prices
This commit is contained in:
parent
1c52af754d
commit
c46a505b4c
15
backend/resources/migrations/003-prices.edn
Normal file
15
backend/resources/migrations/003-prices.edn
Normal 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"]}
|
@ -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)))]
|
||||
|
@ -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"
|
||||
|
@ -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" {})
|
||||
])
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
(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 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"]]])
|
||||
[: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)]))))
|
||||
|
@ -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)))))
|
||||
|
25
frontend/test/chicken_master/stock_test.cljs
Normal file
25
frontend/test/chicken_master/stock_test.cljs
Normal 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}}))))
|
@ -1,5 +1,5 @@
|
||||
[Path]
|
||||
PathModified=/srv/chickens
|
||||
PathModified=/srv/chickens/chicken-master.jar
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user