move order date to recurring

This commit is contained in:
Daniel O'Connell 2022-04-19 21:21:44 +02:00
parent 50ab098410
commit a4bafe61e3
10 changed files with 313 additions and 109 deletions

View File

@ -7,7 +7,9 @@
org.postgresql/postgresql {:mvn/version "42.2.6"} org.postgresql/postgresql {:mvn/version "42.2.6"}
ring-basic-authentication/ring-basic-authentication {:mvn/version "1.1.0"} ring-basic-authentication/ring-basic-authentication {:mvn/version "1.1.0"}
ring-cors/ring-cors {:mvn/version "0.1.13"} ring-cors/ring-cors {:mvn/version "0.1.13"}
ring/ring {:mvn/version "1.8.1"}} ring/ring {:mvn/version "1.8.1"}
org.dmfs/lib-recur {:mvn/version "0.12.2"}
}
:aliases :aliases
{:dev {:jvm-opts ["-Dconfig=config/dev/config.edn"] {:dev {:jvm-opts ["-Dconfig=config/dev/config.edn"]

View File

@ -0,0 +1,14 @@
{:up ["ALTER TABLE orders ADD recurrence VARCHAR(512)"
"ALTER TABLE orders ADD end_date TIMESTAMPTZ"
"CREATE TABLE recurrence_exceptions(
order_id INT,
order_date TIMESTAMPTZ NOT NULL,
status order_state DEFAULT 'waiting',
PRIMARY KEY(order_id, order_date),
CONSTRAINT fk_customer FOREIGN KEY(order_id) REFERENCES orders(id) ON DELETE CASCADE)"
"UPDATE orders SET end_date = o.order_date
FROM orders AS o
WHERE orders.id = o.id"]
:down ["DROP TABLE recurrence_exceptions"
"ALTER TABLE orders DROP COLUMN end_date"
"ALTER TABLE orders DROP COLUMN recurrence"]}

View File

@ -0,0 +1,10 @@
{:up ["INSERT INTO recurrence_exceptions (order_id, order_date, status)
SELECT id AS order_id, order_date, status FROM orders
WHERE status != 'waiting'
ON conflict do nothing"
"ALTER TABLE orders DROP COLUMN status"]
:down ["ALTER TABLE orders ADD status order_state DEFAULT 'waiting'"
"UPDATE orders SET status = ex.status
FROM recurrence_exceptions AS ex
WHERE orders.id = ex.order_id AND orders.order_date = ex.order_date"]}
;

View File

@ -43,8 +43,8 @@
order (-> request :body (update :id #(or % id)))] order (-> request :body (update :id #(or % id)))]
(as-edn (orders/replace! user-id order)))) (as-edn (orders/replace! user-id order))))
(defn delete-order [user-id id] (->> id edn/read-string (orders/delete! user-id) as-edn)) (defn delete-order [user-id id day] (->> id edn/read-string (orders/delete! user-id day) as-edn))
(defn set-order-state [user-id id status] (as-edn (orders/change-state! user-id (edn/read-string id) status))) (defn set-order-state [user-id id day status] (as-edn (orders/change-state! user-id (edn/read-string id) day status)))
(defn get-stock [user-id] (get-values user-id [:customers :products])) (defn get-stock [user-id] (get-values user-id [:customers :products]))
@ -64,5 +64,5 @@
(GET "/orders" [:as {user-id :basic-authentication}] (get-orders user-id)) (GET "/orders" [:as {user-id :basic-authentication}] (get-orders user-id))
(POST "/orders" request (update-order request)) (POST "/orders" request (update-order request))
(PUT "/orders/:id" request (update-order request)) (PUT "/orders/:id" request (update-order request))
(DELETE "/orders/:id" [id :as {user-id :basic-authentication}] (delete-order user-id id)) (DELETE "/orders/:id" [id :as {user-id :basic-authentication body :body}] (delete-order user-id id (:day body)))
(POST "/orders/:id/:status" [id status :as {user-id :basic-authentication}] (set-order-state user-id id status))) (POST "/orders/:id/:status" [id status :as {user-id :basic-authentication body :body}] (set-order-state user-id id (:day body) status)))

View File

@ -7,53 +7,86 @@
[chicken-master.customers :as customers] [chicken-master.customers :as customers]
[chicken-master.time :as t])) [chicken-master.time :as t]))
(defn upsert-order! [tx user-id customer-id {:keys [id day state notes]}] (defn upsert-order! [tx user-id customer-id {:keys [id day notes]}]
(let [order {:customer_id customer-id (let [order {:customer_id customer-id
:notes notes :notes notes
:status (some-> (or state "waiting") name jdbc.types/as-other) :order_date (some-> day t/to-db-date)
:order_date (some-> day t/parse-date t/inst->timestamp)}] :end_date (some-> day t/to-db-date)}]
(if (db/get-by-id tx user-id :orders id) (if (db/get-by-id tx user-id :orders id)
(do (sql/update! tx :orders order {:id id}) id) (do (sql/update! tx :orders order {:id id}) id)
(:orders/id (sql/insert! tx :orders (assoc order :user_id user-id)))))) (:orders/id (sql/insert! tx :orders (assoc order :user_id user-id))))))
(defn structure-order [items] (defn structure-order [items]
{:id (-> items first :orders/id) {:id (-> items first :orders/id)
:notes (-> items first :orders/notes) :notes (-> items first :orders/notes)
:state (-> items first :orders/status keyword) :recurrence (-> items first :orders/recurrence)
:day (-> items first :orders/order_date (.toInstant) str (subs 0 10)) :who {:id (-> items first :customers/id)
:who {:id (-> items first :customers/id) :name (-> items first :customers/name)}
:name (-> items first :customers/name)} :products (->> items
:products (->> items (filter :products/name)
(filter :products/name) (reduce (fn [coll {:keys [order_products/amount order_products/price products/name]}]
(reduce (fn [coll {:keys [order_products/amount order_products/price products/name]}] (assoc coll (keyword name) {:amount amount :price price})) {}))})
(assoc coll (keyword name) {:amount amount :price price})) {}))})
(defn item-days
"Get all days between `from` and `to` (inclusively) for which the order applies."
[from to items]
(let [{:orders/keys [recurrence order_date]} (first items)]
(->> (t/recurrence->dates (t/latest from order_date) (or recurrence "FREQ=MONTHLY;COUNT=1"))
(take-while #(not (t/after % (t/to-inst to))))
(map #(vector (t/format-date %) :waiting))
(into {}))))
(defn order-iterator [items days]
(->> items
(filter :recurrence_exceptions/status)
(reduce (fn [coll {:recurrence_exceptions/keys [status order_date]}]
(assoc coll (t/format-date order_date) (keyword status)))
days)))
(defn items->orders [from to items]
(let [base-order (structure-order items)]
(->> items
(item-days from to)
(order-iterator items)
(map (fn [[date status]] (assoc base-order :day date :state status))))))
(def orders-query (def orders-query
"SELECT o.id, o.notes, o.status, o.order_date, c.id, c.name, p.name, op.amount, op.price "SELECT o.id, o.notes, ex.status, o.order_date, o.recurrence, c.id, c.name, p.name, op.amount, op.price, ex.order_date
FROM orders o JOIN customers c ON o.customer_id = c.id FROM orders o JOIN customers c ON o.customer_id = c.id
LEFT OUTER JOIN recurrence_exceptions ex ON o.id = ex.order_id
LEFT OUTER JOIN order_products op ON o.id = op.order_id LEFT OUTER JOIN order_products op ON o.id = op.order_id
LEFT OUTER JOIN products p on p.id = op.product_id ") LEFT OUTER JOIN products p on p.id = op.product_id ")
(defn- get-orders [tx where params] (def date-filter-clause "WHERE o.order_date >= ? AND o.end_date <= ? ")
(->> (into [(if where (str orders-query where) orders-query)] params) (def orders-date-query (str orders-query date-filter-clause))
(sql/query tx)
(group-by :orders/id)
vals
(map structure-order)))
(defn get-order [tx user-id id] (defn- get-orders
(first (get-orders tx "WHERE o.id = ? AND o.user_id = ?" [id user-id]))) ([tx where params] (get-orders tx t/min-date t/max-date where params))
([tx from to where params]
(->> (into [(str orders-date-query (if where (str " AND " where) ""))
(t/to-db-date from)
(t/to-db-date to)] params)
(sql/query tx)
(group-by :orders/id)
vals
(map (partial items->orders from to))
(apply concat)
(filter #(t/between from (:day %) to)))))
(defn get-all [user-id] (group-by :day (get-orders db/db-uri "WHERE o.user_id = ?" [user-id]))) (defn get-order [tx user-id id & [day]]
(first
(if day
(->> (get-orders tx day day "o.id = ? AND o.user_id = ?" [id user-id])
(filter #(= (t/format-date day) (:day %))))
(get-orders tx "o.id = ? AND o.user_id = ?" [id user-id]))))
(defn get-all [user-id] (group-by :day (get-orders db/db-uri "o.user_id = ?" [user-id])))
(defn- orders-for-days [tx user-id & days] (defn- orders-for-days [tx user-id & days]
(let [days (remove nil? days)] (let [days (->> days (remove nil?) (map t/to-inst))
(->> days from (apply t/earliest days)
(map t/inst->timestamp) to (apply t/latest days)]
(map jdbc.types/as-date) (->> (get-orders tx from to "o.user_id = ?" [user-id])
(into [user-id])
(get-orders tx (str "WHERE o.user_id = ? AND o.order_date::date IN " (db/psql-list days)))
(group-by :day) (group-by :day)
(merge (reduce #(assoc %1 (t/format-date %2) {}) {} days))))) (merge (reduce #(assoc %1 (t/format-date %2) {}) {} days)))))
@ -61,32 +94,59 @@
(jdbc/with-transaction [tx db/db-uri] (jdbc/with-transaction [tx db/db-uri]
(let [customer-id (or (:id who) (let [customer-id (or (:id who)
(customers/get-or-create-by-name tx user-id (:name who))) (customers/get-or-create-by-name tx user-id (:name who)))
previous-day (some->> order :id (db/get-by-id tx user-id :orders) :orders/order_date (.toInstant))] previous-day (some->> order :id (db/get-by-id tx user-id :orders) :orders/order_date t/to-inst)]
(products/update-products-mapping! tx user-id :order (products/update-products-mapping! tx user-id :order
(upsert-order! tx user-id customer-id order) (upsert-order! tx user-id customer-id order)
products) products)
(orders-for-days tx user-id previous-day (some-> order :day t/parse-date))))) (orders-for-days tx user-id previous-day (some-> order :day t/parse-date)))))
(defn delete! [user-id id]
(jdbc/with-transaction [tx db/db-uri]
(let [day (some->> id (db/get-by-id tx user-id :orders) :orders/order_date (.toInstant))]
(sql/delete! tx :orders {:id id :user_id user-id})
(when day (orders-for-days tx user-id day)))))
(defn change-state! (defn change-state!
"Update the state of the given order and also modify the number of products available: "Update the state of the given order and also modify the number of products available:
* when `fulfilled` decrement the number of products * when `fulfilled` decrement the number of products
* when `waiting` increment the number (as this means a previously fulfilled order has been returned)" * when `waiting` increment the number (as this means a previously fulfilled order has been returned)"
[user-id id state] ([user-id id day state] (jdbc/with-transaction [tx db/db-uri] (change-state! tx user-id id day state)))
(jdbc/with-transaction [tx db/db-uri] ([tx user-id id day state]
(let [order (get-order tx user-id id) (let [order (get-order tx user-id id day)
operator (condp = state operator (condp = state
"fulfilled" "-" "fulfilled" "-"
"waiting" "+")] "waiting" "+"
(when (not= (:state order) (keyword state)) "canceled" "+")]
(doseq [[prod {:keys [amount]}] (:products order)] (when (not= (:state order) (keyword state))
(jdbc/execute-one! tx ;; update product counts
[(str "UPDATE products SET amount = amount " operator " ? WHERE name = ?") (doseq [[prod {:keys [amount]}] (:products order)]
(jdbc/execute-one! tx
[(str "UPDATE products SET amount = amount " operator " ? WHERE name = ?")
amount (name prod)])) amount (name prod)]))
(sql/update! tx :orders {:status (jdbc.types/as-other state)} {:id id}))
(orders-for-days tx user-id (-> order :day t/parse-date))))) ;; upsert the state for the given day
(if (jdbc/execute-one! tx
["SELECT * from recurrence_exceptions WHERE order_id = ? AND order_date = ?" id (t/to-db-date day)])
(sql/update! tx :recurrence_exceptions {:status (jdbc.types/as-other state)}
{:order_id id :order_date (t/to-db-date day)})
(sql/insert! tx :recurrence_exceptions {:order_id id
:order_date (t/to-db-date day)
:status (jdbc.types/as-other state)})))
(orders-for-days tx user-id day))))
(defn delete! [user-id day id]
(jdbc/with-transaction [tx db/db-uri]
(if day
;; Only delete the one day
(change-state! tx user-id id day "canceled")
;; Delete the order along with all recurrences
(when-let [{:orders/keys [order_date end_date]} (some->> id (db/get-by-id tx user-id :orders))]
(sql/delete! tx :orders {:id id :user_id user-id})
(orders-for-days tx user-id order_date end_date)))))
;; (delete! 2 "2022-04-20" 240)
;; (delete! 2 nil 241)
;; (change-state! 2 240 "2022-04-20" "waiting")
;; (change-state! 2 250 "2022-04-23" "fulfilled")
;; (get-orders db/db-uri (t/to-inst #inst "2022-04-20T00:00:00Z") (t/to-inst #inst "2022-04-20T00:00:00Z") nil nil)
;; (get-orders db/db-uri (t/to-inst #inst "2022-04-23T00:00:00Z") (t/to-inst #inst "2022-04-24T00:00:00Z") nil nil)
;; (get-order db/db-uri 2 242 (t/to-inst #inst "2022-04-20T00:00:00Z"))
;; (orders-for-days db/db-uri 2 #inst "2022-04-23T00:00:00Z" #inst "2022-04-23T00:00:00Z")
;; (orders-for-days db/db-uri 2 #inst "2022-04-23T00:00:00Z")
;; (orders-for-days db/db-uri 2 "2022-04-19")
;; (get-all 2)

View File

@ -1,17 +1,59 @@
(ns chicken-master.time (ns chicken-master.time
(:import [java.time Instant LocalDate ZoneOffset] (:import [java.time Instant LocalDate ZoneOffset]
[java.time.format DateTimeFormatter] [java.time.format DateTimeFormatter]
[java.sql Timestamp])) [java.sql Timestamp]
[org.dmfs.rfc5545.recur RecurrenceRule]
[org.dmfs.rfc5545 DateTime]))
(defn recurrence->dates [start rule]
(let [iterator (.iterator (RecurrenceRule. rule)
(-> start (.toEpochMilli) (DateTime.)))]
(take-while identity
(repeatedly #(when (.hasNext iterator)
(-> iterator (.nextDateTime) (.getTimestamp) (Instant/ofEpochMilli)))))))
(defn parse-date [date] (defn parse-date [date]
(-> date (LocalDate/parse) (.atStartOfDay) (.toInstant ZoneOffset/UTC))) (if (= (count date) 10)
(-> date (LocalDate/parse) (.atStartOfDay) (.toInstant ZoneOffset/UTC))
(Instant/parse date)))
(defn format-date [date] (defprotocol TimeHelpers
(-> DateTimeFormatter/ISO_LOCAL_DATE (to-inst [d])
(.withZone ZoneOffset/UTC) (to-db-date [d])
(.format date))) (format-date [date])
(before [d1 d2])
(after [d1 d2]))
(defn inst->timestamp [inst] (Timestamp/from inst)) (extend-type Instant
TimeHelpers
(to-inst [d] d)
(to-db-date [d] (Timestamp/from d))
(format-date [date]
(-> DateTimeFormatter/ISO_LOCAL_DATE
(.withZone ZoneOffset/UTC)
(.format date)))
(before [d1 d2] (.isBefore d1 d2))
(after [d1 d2] (.isBefore d2 d1)))
(extend-type java.util.Date
TimeHelpers
(to-inst [d] (.toInstant d))
(to-db-date [d] (-> d to-inst to-db-date))
(format-date [date] (format-date (to-inst date)))
(before [d1 d2] (< (.compareTo d1 d2) 0))
(after [d1 d2] (> (.compareTo d1 d2) 0)))
(extend-type java.lang.String
TimeHelpers
(to-inst [d] (parse-date d))
(to-db-date [d] (-> d to-inst to-db-date))
(format-date [date] (format-date (to-inst date)))
(before [d1 d2] (before (to-inst d1) (to-inst d2)))
(after [d1 d2] (after (to-inst d1) (to-inst d2))))
(defn earliest [& ds] (->> ds (map to-inst) (sort before) first))
(defn latest [& ds] (->> ds (map to-inst) (sort after) first))
(defn between [d1 d2 d3] (and (not (before d2 d1)) (not (after d2 d3))))
(defn now [] (Instant/now)) (defn now [] (Instant/now))
(def min-date (parse-date "2020-01-01"))
(def max-date (.plusSeconds (now) (* 10 356 24 60 60))) ; 10 years from now - can't be bothered to do this properly...

View File

@ -2,8 +2,10 @@
(:require (:require
[next.jdbc :as jdbc] [next.jdbc :as jdbc]
[next.jdbc.sql :as sql] [next.jdbc.sql :as sql]
[next.jdbc.types :as jdbc.types]
[chicken-master.orders :as sut] [chicken-master.orders :as sut]
[chicken-master.products :as products] [chicken-master.products :as products]
[chicken-master.time :as t]
[clojure.string :as str] [clojure.string :as str]
[clojure.test :refer [deftest is testing]])) [clojure.test :refer [deftest is testing]]))
@ -13,53 +15,55 @@
products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}}] products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}}]
(if products (if products
(for [[product {:keys [amount price]}] products] (for [[product {:keys [amount price]}] products]
(merge #:orders{:id id :notes notes :status status :order_date date} (merge #:orders{:id id :notes notes :order_date date :end_date date}
#:recurrence_exceptions{:order_id id :order_date date :status status}
#:customers{:id user_id :name user_name} #:customers{:id user_id :name user_name}
{:products/name (name product) :order_products/price price :order_products/amount amount})) {:products/name (name product) :order_products/price price :order_products/amount amount}))
[(merge #:orders{:id id :notes notes :status status :order_date date} [(merge #:orders{:id id :notes notes :order_date date :end_date date}
#:recurrence_exceptions{:order_id id :order_date date :status status}
#:customers{:id user_id :name user_name} #:customers{:id user_id :name user_name}
{:products/name nil :order_products/price nil :order_products/amount nil})])) {:products/name nil :order_products/price nil :order_products/amount nil})]))
(deftest structure-order-test (deftest structure-order-test
(testing "basic structure" (testing "basic structure"
(is (= (sut/structure-order (raw-order-row)) (is (= (sut/structure-order (raw-order-row))
{:id 1, :notes "note", :state :pending, :day "2020-01-01", {:id 1, :notes "note", :recurrence nil,
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"},
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}))) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}})))
(testing "missing products" (testing "missing products"
(is (= (sut/structure-order (raw-order-row :products nil)) (is (= (sut/structure-order (raw-order-row :products nil))
{:id 1, :notes "note", :state :pending, :day "2020-01-01", {:id 1, :notes "note", :recurrence nil
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"},
:products {}})))) :products {}}))))
(deftest test-get-order (deftest test-get-order
(testing "correct values returned" (testing "correct values returned"
(with-redefs [sql/query (fn [_ [query & params]] (with-redefs [sql/query (fn [_ [query & params]]
(is (str/ends-with? query "WHERE o.id = ? AND o.user_id = ?")) (is (str/ends-with? query "WHERE o.order_date >= ? AND o.end_date <= ? AND o.id = ? AND o.user_id = ?"))
(is (= params [123 "1"])) (is (= params [(t/to-db-date t/min-date) (t/to-db-date t/max-date) 123 "1"]))
(raw-order-row))] (raw-order-row))]
(is (= (sut/get-order :tx "1" 123) (is (= (sut/get-order :tx "1" 123)
{:id 1, :notes "note", :state :pending, :day "2020-01-01", {:id 1, :notes "note", :recurrence nil :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"},
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}})))) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}))))
(testing "Only 1 item returned" (testing "Only 1 item returned"
(with-redefs [sql/query (fn [_ [query & params]] (with-redefs [sql/query (fn [_ [query & params]]
(is (str/ends-with? query "WHERE o.id = ? AND o.user_id = ?")) (is (str/ends-with? query "WHERE o.order_date >= ? AND o.end_date <= ? AND o.id = ? AND o.user_id = ?"))
(is (= params [123 "1"])) (is (= params [(t/to-db-date t/min-date) (t/to-db-date t/max-date) 123 "1"]))
(concat (raw-order-row) (concat (raw-order-row)
(raw-order-row :id 21)))] (raw-order-row :id 21)))]
(is (= (sut/get-order :tx "1" 123) (is (= (sut/get-order :tx "1" 123)
{:id 1, :notes "note", :state :pending, :day "2020-01-01", {:id 1, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}))))) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}})))))
(deftest test-get-all (deftest test-get-all
(testing "correct values returned" (testing "correct values returned"
(with-redefs [sql/query (fn [_ [query & params]] (with-redefs [sql/query (fn [_ [query & params]]
(is (str/ends-with? query "WHERE o.user_id = ?")) (is (str/ends-with? query "WHERE o.order_date >= ? AND o.end_date <= ? AND o.user_id = ?"))
(is (= params ["1"])) (is (= params [(t/to-db-date t/min-date) (t/to-db-date t/max-date) "1"]))
(concat (concat
(raw-order-row :id 1 :status "waiting") (raw-order-row :id 1 :status "waiting")
(raw-order-row :id 2 :date #inst "2020-01-03") (raw-order-row :id 2 :date #inst "2020-01-03")
@ -67,16 +71,16 @@
(raw-order-row :id 4)))] (raw-order-row :id 4)))]
(is (= (sut/get-all "1") (is (= (sut/get-all "1")
{"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01", {"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}} :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}
{:id 3, :notes "note", :state :pending, :day "2020-01-01", {:id 3, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 43, :name "John"}, :who {:id 43, :name "John"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}} :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}
{:id 4, :notes "note", :state :pending, :day "2020-01-01", {:id 4, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}] :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]
"2020-01-03" [{:id 2, :notes "note", :state :pending, :day "2020-01-03", "2020-01-03" [{:id 2, :notes "note", :state :pending, :day "2020-01-03",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))))) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]})))))
(deftest test-replace! (deftest test-replace!
@ -99,10 +103,10 @@
(raw-order-row :id 4)))] (raw-order-row :id 4)))]
(is (= (sut/replace! :user-id order) (is (= (sut/replace! :user-id order)
{"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01", {"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}} :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}
{:id 4, :notes "note", :state :pending, :day "2020-01-01", {:id 4, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))))) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]})))))
(testing "replace order from different day" (testing "replace order from different day"
@ -124,10 +128,10 @@
(raw-order-row :id 4)))] (raw-order-row :id 4)))]
(is (= (sut/replace! :user-id order) (is (= (sut/replace! :user-id order)
{"2020-01-01" [{:id 4, :notes "note", :state :pending, :day "2020-01-01", {"2020-01-01" [{:id 4, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}] :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]
"2020-01-02" [{:id 1, :notes "note", :state :waiting, :day "2020-01-02", "2020-01-02" [{:id 1, :notes "note", :state :waiting, :day "2020-01-02",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))))) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]})))))
(testing "unknown products are ignored" (testing "unknown products are ignored"
@ -149,10 +153,10 @@
(raw-order-row :id 4)))] (raw-order-row :id 4)))]
(is (= (sut/replace! :user-id order) (is (= (sut/replace! :user-id order)
{"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01", {"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}} :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}
{:id 4, :notes "note", :state :pending, :day "2020-01-01", {:id 4, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]})))))) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))))))
(deftest test-delete! (deftest test-delete!
@ -163,9 +167,9 @@
(is (= table :orders)) (is (= table :orders))
(is (= by {:id 1 :user_id :user-id}))) (is (= by {:id 1 :user_id :user-id})))
sql/query (constantly (raw-order-row :id 4))] sql/query (constantly (raw-order-row :id 4))]
(is (= (sut/delete! :user-id 1) (is (= (sut/delete! :user-id nil 1)
{"2020-01-01" [{:id 4, :notes "note", :state :pending, :day "2020-01-01", {"2020-01-01" [{:id 4, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]})))) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))))
(testing "nothing returned if no date set for the given order" (testing "nothing returned if no date set for the given order"
@ -175,32 +179,102 @@
(is (= table :orders)) (is (= table :orders))
(is (= by {:id 1 :user_id :user-id}))) (is (= by {:id 1 :user_id :user-id})))
sql/query (constantly (raw-order-row :id 4))] sql/query (constantly (raw-order-row :id 4))]
(is (nil? (sut/delete! :user-id 1)))))) (is (nil? (sut/delete! :user-id nil 1)))))
(let [invocations (atom [])]
(with-redefs [jdbc.types/as-other identity
jdbc/transact (fn [_ f & args] (apply f args))
jdbc/execute-one! (constantly {:orders/order_date #inst "2020-01-01"})
sql/delete! (fn [_ table by]
(swap! invocations conj ["deleting" table by]))
sql/query (constantly (raw-order-row :id 4))
sql/update! (fn [_ table status key] (swap! invocations conj ["updating" table status key]))
sql/insert! (fn [_ table values] (swap! invocations conj ["inserting" table values]))]
(testing "deleting without provided a date will remove the whole order"
(reset! invocations [])
(is (= (sut/delete! :user-id nil 1)
{"2020-01-01" [{:id 4, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))
(is (= [["deleting" :orders {:id 1 :user_id :user-id}]]
@invocations)))
(testing "deleting with a provided date will soft remove a single order by updating it if it exists"
(reset! invocations [])
(is (= (sut/delete! :user-id "2020-01-01" 1)
{"2020-01-01" [{:id 4, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))
(is (= [["updating" :recurrence_exceptions {:status "canceled"}
{:order_id 1, :order_date (t/to-db-date "2020-01-01")}]]
@invocations)))
(testing "deleting with a provided date will soft remove a single order by adding an exception if none provided"
(with-redefs [jdbc/execute-one! (fn [_ [q]]
(when-not (str/includes? q "recurrence_exceptions")
{:orders/order_date #inst "2020-01-01"}))]
(reset! invocations [])
(is (= (sut/delete! :user-id "2020-01-01" 1)
{"2020-01-01" [{:id 4, :notes "note", :state :pending, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))
(is (= [["inserting" :recurrence_exceptions {:order_id 1, :order_date (t/to-db-date "2020-01-01") :status "canceled"}]]
@invocations)))))))
(deftest test-change-state! (deftest test-change-state!
(testing "states get changed" (let [updates (atom [])]
(let [updates (atom [])] (with-redefs [jdbc.types/as-other identity
(with-redefs [jdbc/transact (fn [_ f & args] (apply f args)) jdbc/transact (fn [_ f & args] (apply f args))
jdbc/execute-one! #(swap! updates conj %2) jdbc/execute-one! #(swap! updates conj %2)
sql/update! (fn [_ table _ val] sql/update! (fn [_ table _ val]
(is (= table :orders)) (swap! updates conj ["updating" table val]))
(is (= val {:id 1}))) sql/query (constantly (raw-order-row :id 1 :status "waiting"))
sql/query (constantly (raw-order-row :id 1 :status "waiting"))] sql/insert! (fn [_ table values] (swap! updates conj ["inserting" table values]))]
(is (= (sut/change-state! :user-id 1 "fulfilled") (testing "states get changed - update when prexisiting exception"
(reset! updates [])
(is (= (sut/change-state! :user-id 1 "2020-01-01" "fulfilled")
{"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01", {"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]})) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))
(is (= @updates [["UPDATE products SET amount = amount - ? WHERE name = ?" 12 "eggs"] (is (= [;; product updates
["UPDATE products SET amount = amount - ? WHERE name = ?" 3 "milk"]]))))) ["UPDATE products SET amount = amount - ? WHERE name = ?" 12 "eggs"]
["UPDATE products SET amount = amount - ? WHERE name = ?" 3 "milk"]
;; check whether to insert or update
["SELECT * from recurrence_exceptions WHERE order_id = ? AND order_date = ?" 1 (t/to-db-date "2020-01-01")]
;; update
["updating" :recurrence_exceptions {:order_id 1, :order_date (t/to-db-date "2020-01-01")}]]
@updates)))
(testing "states get changed - insert when no such exception"
(with-redefs [jdbc/execute-one! (fn [_ q]
(swap! updates conj q)
(when-not (str/includes? (first q) "recurrence_exceptions")
(raw-order-row :id 1 :status "waiting")))]
(reset! updates [])
(is (= (sut/change-state! :user-id 1 "2020-01-01" "fulfilled")
{"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))
(is (= [;; product updates
["UPDATE products SET amount = amount - ? WHERE name = ?" 12 "eggs"]
["UPDATE products SET amount = amount - ? WHERE name = ?" 3 "milk"]
;; check whether to insert or update
["SELECT * from recurrence_exceptions WHERE order_id = ? AND order_date = ?" 1 (t/to-db-date "2020-01-01")]
;; update
["inserting" :recurrence_exceptions {:order_id 1, :order_date (t/to-db-date "2020-01-01"), :status "fulfilled"}]]
@updates))))))
(testing "nothing happens if the state is already set" (testing "nothing happens if the state is already set"
(let [updates (atom [])] (let [updates (atom [])]
(with-redefs [jdbc/transact (fn [_ f & args] (apply f args)) (with-redefs [jdbc/transact (fn [_ f & args] (apply f args))
jdbc/execute-one! #(swap! updates conj %2) jdbc/execute-one! #(swap! updates conj %2)
sql/query (constantly (raw-order-row :id 1 :status "waiting"))] sql/query (constantly (raw-order-row :id 1 :status "waiting"))]
(is (= (sut/change-state! :user-id 1 "waiting") (is (= (sut/change-state! :user-id 1 "2020-01-01" "waiting")
{"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01", {"2020-01-01" [{:id 1, :notes "note", :state :waiting, :day "2020-01-01",
:who {:id 2, :name "mr blobby"}, :who {:id 2, :name "mr blobby"}, :recurrence nil
:products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]})) :products {:eggs {:amount 12 :price nil} :milk {:amount 3 :price 423}}}]}))
(is (= @updates []))))) (is (= @updates [])))))

View File

@ -89,8 +89,8 @@
:on-drag-start #(-> % .-dataTransfer (.setData "text" id))} :on-drag-start #(-> % .-dataTransfer (.setData "text" id))}
[:div {:class :actions} [:div {:class :actions}
(condp = state (condp = state
:waiting [:button {:on-click #(re-frame/dispatch [::event/fulfill-order id])} "✓"] :waiting [:button {:on-click #(re-frame/dispatch [::event/fulfill-order id day])} "✓"]
:fulfilled [:button {:on-click #(re-frame/dispatch [::event/reset-order id])} "X"] :fulfilled [:button {:on-click #(re-frame/dispatch [::event/reset-order id day])} "X"]
:pending nil :pending nil
nil nil) nil nil)
[:button {:on-click #(re-frame/dispatch [::event/edit-order day id])} "E"] [:button {:on-click #(re-frame/dispatch [::event/edit-order day id])} "E"]
@ -120,6 +120,7 @@
(->> (if (settings :hide-fulfilled-orders) (->> (if (settings :hide-fulfilled-orders)
(remove (comp #{:fulfilled} :state) orders) (remove (comp #{:fulfilled} :state) orders)
orders) orders)
(remove (comp #{:canceled} :state))
(map (partial format-order settings)) (map (partial format-order settings))
doall) doall)
(when (settings :show-day-add-order) (when (settings :show-day-add-order)

View File

@ -46,7 +46,6 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::load-db ::load-db
(fn [_ _] (fn [_ _]
(prn "loading")
(time/update-settings config/default-settings) (time/update-settings config/default-settings)
{:fx [[:dispatch [::show-from-date (time/iso-date (time/today))]] {:fx [[:dispatch [::show-from-date (time/iso-date (time/today))]]
[:dispatch [::start-loading]] [:dispatch [::start-loading]]
@ -98,15 +97,17 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::fulfill-order ::fulfill-order
(fn [{db :db} [_ id]] (fn [{db :db} [_ id day]]
{:db (assoc-in db [:orders id :state] :pending) {:db (assoc-in db [:orders id :state] :pending)
:http-xhrio (http-request :post (str "orders/" id "/fulfilled"))})) :http-xhrio (http-request :post (str "orders/" id "/fulfilled")
:body {:day day})}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::reset-order ::reset-order
(fn [{db :db} [_ id]] (fn [{db :db} [_ id day]]
{:db (assoc-in db [:orders id :state] :waiting) {:db (assoc-in db [:orders id :state] :waiting)
:http-xhrio (http-request :post (str "orders/" id "/waiting"))})) :http-xhrio (http-request :post (str "orders/" id "/waiting")
:body {:day day})}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::save-order ::save-order

View File

@ -47,4 +47,4 @@
[:h2 "Magazyn"] [:h2 "Magazyn"]
[stock-form @(re-frame/subscribe [::subs/available-products])]] [stock-form @(re-frame/subscribe [::subs/available-products])]]
;; On success ;; On success
:on-submit (fn [form] (prn form) (prn (process-form form)) (re-frame/dispatch [::event/save-stock (process-form form)])))) :on-submit (fn [form] (re-frame/dispatch [::event/save-stock (process-form form)]))))