Repository: CSCfi/rems
Pull request #1533 (2019-08-22 14:59:35)

Refactor creating an application to be a command
Followup from #1519/#832

# Definition of Done / Review checklist

## Reviewability
- [x] link to issue
- [x] note if PR is on top of other PR
- [x] note if related change in rems-deploy repo
- [x] consider adding screenshots for ease of review

## API
- [x] API is documented and shows up in Swagger UI
- [x] API is backwards compatible or completely new
- [x] Events are backwards compatible

## Documentation
- [x] update changelog if necessary
- [x] add or update docstrings for namespaces and functions
- [x] components are added to guide page
- [x] documentation _at least_ for config options (i.e. docs folder)
- [x] ADR for major architectural decisions or experiments

## Different installations
- [x] new configuration options added to rems-deploy repository
- [x] instance specific translations (i.e. LBR kielivara)

## Testing
- [x] complex logic is unit tested
- [x] valuable features are integration / browser / acceptance tested automatically

## Accessibility
- [x] all icons have the aria-label attribute
- [x] all fields have a label
- [x] errors are linked to fields with aria-describedby
- [x] contrast is checked
- [x] conscious decision about where to move focus after an action

## Follow-up
- [x] new tasks are created for pending or remaining tasks
- [x] no critical TODOs left to implement



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a241c70a2..548fd3f71 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,11 @@ have notable changes.
 
 Changes since v2.4
 
+### Breaking changes
+- `:application/external-id` has been made a non-optional field in the
+  API and event schemas. All applications should already have an external ID
+  since the previous release, so no database migration should be needed. 
+
 ### Additions
 - New field types: description, option, multiselect
 - Setting maximum length for a form field
diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md
deleted file mode 100644
index 1ceaa8c5c..000000000
--- a/docs/pull_request_template.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Definition of Done / Review checklist
-
-## Reviewability
-- [ ] link to issue
-- [ ] note if PR is on top of other PR
-- [ ] note if related change in rems-deploy repo
-- [ ] consider adding screenshots for ease of review
-
-## API
-- [ ] API is documented and shows up in Swagger UI
-- [ ] API is backwards compatible or completely new
-- [ ] Events are backwards compatible
-
-## Documentation
-- [ ] update changelog if necessary
-- [ ] add or update docstrings for namespaces and functions
-- [ ] components are added to guide page
-- [ ] documentation _at least_ for config options (i.e. docs folder)
-- [ ] ADR for major architectural decisions or experiments
-
-## Different installations
-- [ ] new configuration options added to rems-deploy repository
-- [ ] instance specific translations (i.e. LBR kielivara)
-
-## Testing
-- [ ] complex logic is unit tested
-- [ ] valuable features are integration / browser / acceptance tested automatically
-
-## Accessibility
-- [ ] all icons have the aria-label attribute
-- [ ] all fields have a label
-- [ ] errors are linked to fields with aria-describedby
-- [ ] contrast is checked
-- [ ] conscious decision about where to move focus after an action
-
-## Follow-up
-- [ ] new tasks are created for pending or remaining tasks
-- [ ] no critical TODOs left to implement
diff --git a/project.clj b/project.clj
index 3f49f7be5..b4345268d 100644
--- a/project.clj
+++ b/project.clj
@@ -5,70 +5,70 @@
 
   :dependencies [[ch.qos.logback/logback-classic "1.2.3"]
                  [clj-commons/secretary "1.2.4"]
-                 [clj-http "3.10.0"]
-                 [clj-pdf "2.3.9"]
-                 [clj-time "0.15.2"]
+                 [clj-http "3.9.1"]
+                 [clj-pdf "2.3.1"]
+                 [clj-time "0.15.1"]
                  [cljs-ajax "0.8.0"]
-                 [cljsjs/react "16.9.0-0"] ; overrides the old version used by Reagent
-                 [cljsjs/react-dom "16.9.0-0"]
-                 [cljsjs/react-dom-server "16.9.0-0"]
+                 [cljsjs/react "16.8.3-0"] ; overrides the old version used by Reagent
+                 [cljsjs/react-dom "16.8.3-0"]
+                 [cljsjs/react-dom-server "16.8.3-0"]
                  [cljsjs/react-select "2.4.4-0"]
                  [com.andrewmcveigh/cljs-time "0.5.2"]
                  [com.attendify/schema-refined "0.3.0-alpha4"]
                  [com.draines/postal "2.0.3"]
-                 [com.fasterxml.jackson.datatype/jackson-datatype-joda "2.10.0.pr1"]
+                 [com.fasterxml.jackson.datatype/jackson-datatype-joda "2.9.7"]
                  [com.taoensso/tempura "1.2.1"]
                  [compojure "1.6.1"]
                  [conman "0.8.3"]
-                 [cprop "0.1.14"]
-                 [garden "1.3.9"]
+                 [cprop "0.1.13"]
                  [haka-buddy "0.2.3" :exclusions [cheshire]]
                  [hiccup "1.0.5"]
-                 [javax.xml.bind/jaxb-api "2.4.0-b180830.0359"]
-                 [lambdaisland/deep-diff "0.0-47"]
+                 [javax.xml.bind/jaxb-api "2.3.1"]
+                 [lambdaisland/deep-diff "0.0-25"]
                  [luminus-jetty "0.1.7"]
-                 [luminus-migrations "0.6.5"]
-                 [luminus-nrepl "0.1.6"]
-                 [luminus/ring-ttl-session "0.3.3"]
+                 [luminus-migrations "0.6.3"]
+                 [luminus-nrepl "0.1.5"]
+                 [luminus/ring-ttl-session "0.3.2"]
                  [macroz/hiccup-find "0.6.1"]
-                 [markdown-clj "1.10.0"]
+                 [markdown-clj "1.0.7"]
                  [medley "1.2.0"]
                  [metosin/compojure-api "2.0.0-alpha28" :exclusions [cheshire com.fasterxml.jackson.core/jackson-core]]
-                 [metosin/jsonista "0.2.4"]
-                 [metosin/komponentit "0.3.9"]
+                 [metosin/jsonista "0.2.2"]
+                 [metosin/komponentit "0.3.7"]
                  [metosin/ring-swagger "0.26.2"]
                  [metosin/ring-swagger-ui "3.20.1"]
-                 [mount "0.1.16"]
-                 [ns-tracker "0.4.0"]
+                 [mount "0.1.15"]
                  [org.apache.lucene/lucene-core "8.2.0"]
                  [org.apache.lucene/lucene-queryparser "8.2.0"]
+                 [org.clojars.luontola/garden "1.3.6-patch1"] ;; TODO: waiting for a new release with https://github.com/noprompt/garden/pull/172
+                 [org.clojars.luontola/ns-tracker "0.3.1-patch3"] ;; TODO: waiting for a new release with https://github.com/weavejester/ns-tracker/pull/24
                  [org.clojars.pntblnk/clj-ldap "0.0.16"]
                  [org.clojars.runa/conjure "2.2.0"]
                  [org.clojure/clojure "1.10.0"]
                  [org.clojure/clojurescript "1.10.520" :exclusions [com.fasterxml.jackson.core/jackson-core]]
                  [org.clojure/core.cache "0.7.2"]
-                 [org.clojure/core.memoize "0.7.2"]
-                 [org.clojure/tools.cli "0.4.2"]
-                 [org.clojure/tools.logging "0.5.0"]
-                 [org.postgresql/postgresql "42.2.6"]
+                 [org.clojure/core.memoize "0.7.1"]
+                 [org.clojure/tools.cli "0.4.1"]
+                 [org.clojure/tools.logging "0.4.1"]
+                 [org.postgresql/postgresql "42.2.5"]
                  [org.webjars.bower/tether "1.4.4"]
                  [org.webjars.npm/better-dateinput-polyfill "3.0.0"]
                  [org.webjars.npm/better-dom "4.0.0"]
                  [org.webjars.npm/diff-match-patch "1.0.4"]
-                 [org.webjars.npm/popper.js "1.15.0"]
-                 [org.webjars/bootstrap "4.3.1"]
-                 [org.webjars/font-awesome "5.10.1"]
-                 [org.webjars/jquery "3.4.1"]
-                 [prismatic/schema-generators "0.1.3"]
-                 [re-frame "0.10.8"]
+                 [org.webjars.npm/popper.js "1.14.6"]
+                 [org.webjars/bootstrap "4.2.1"]
+                 [org.webjars/font-awesome "5.6.3"]
+                 [org.webjars/jquery "3.3.1-1"]
+                 [prismatic/schema-generators "0.1.2"]
+                 [re-frame "0.10.6"]
                  [reagent "0.8.1"]
-                 [reagent-utils "0.3.3"]
-                 [ring-cors "0.1.13"]
-                 [ring-middleware-format "0.7.4"]
+                 [reagent-utils "0.3.2"]
+                 [ring-cors "0.1.12"]
+                 [ring-middleware-format "0.7.3"]
                  [ring-webjars "0.2.0"]
                  [ring/ring-core "1.7.1"]
                  [ring/ring-defaults "0.3.2"]
-                 [ring/ring-devel "1.7.1"]
+                 [ring/ring-devel "1.7.1" :exclusions [ns-tracker]]
                  [ring/ring-servlet "1.7.1"]]
 
   :min-lein-version "2.0.0"
@@ -144,17 +144,17 @@
    :test [:project/dev :project/test :profiles/test]
 
    :project/dev {:dependencies [[binaryage/devtools "0.9.10"]
-                                [cider/piggieback "0.4.1"]
+                                [cider/piggieback "0.3.10"]
                                 [com.clojure-goes-fast/clj-memory-meter "0.1.2"]
-                                [criterium "0.4.5"]
+                                [criterium "0.4.4"]
                                 [doo "0.1.11"]
-                                [lambdaisland/kaocha "0.0-529"]
-                                [etaoin "0.3.5"]
-                                [figwheel-sidecar "0.5.19" :exclusions [org.clojure/tools.nrepl org.clojure/core.async com.fasterxml.jackson.core/jackson-core]]
+                                [lambdaisland/kaocha "0.0-418"]
+                                [etaoin "0.3.1"]
+                                [figwheel-sidecar "0.5.18" :exclusions [org.clojure/tools.nrepl org.clojure/core.async com.fasterxml.jackson.core/jackson-core]]
                                 [org.clojure/core.rrb-vector "0.0.14"] ;; the version doo pulls in is broken on fresh cljs
-                                [re-frisk "0.5.4.1"]
-                                [ring/ring-mock "0.4.0" :exclusions [cheshire]]
-                                [se.haleby/stub-http "0.2.7"]]
+                                [re-frisk "0.5.4"]
+                                [ring/ring-mock "0.3.2" :exclusions [cheshire]]
+                                [se.haleby/stub-http "0.2.5"]]
 
                  :plugins [[lein-ancient "0.6.15"]
                            [lein-doo "0.1.11"]
diff --git a/src/clj/rems/api/applications.clj b/src/clj/rems/api/applications.clj
index c0d3115a6..2e7aaefc2 100644
--- a/src/clj/rems/api/applications.clj
+++ b/src/clj/rems/api/applications.clj
@@ -187,7 +187,7 @@
       :roles #{:logged-in}
       :body [request CreateApplicationCommand]
       :return CreateApplicationResponse
-      (ok (applications/create-application! (getx-user-id) (:catalogue-item-ids request))))
+      (ok (api-command :application.command/create request)))
 
     (POST "/copy-as-new" []
       :summary "Create a new application as a copy of an existing application."
diff --git a/src/clj/rems/api/schema.clj b/src/clj/rems/api/schema.clj
index 7a6231df3..7b2ae9c63 100644
--- a/src/clj/rems/api/schema.clj
+++ b/src/clj/rems/api/schema.clj
@@ -156,15 +156,15 @@
 
 (s/defschema Application
   {:application/id s/Int
-   :application/external-id (s/maybe s/Str)
+   :application/external-id s/Str
    :application/state s/Keyword
    :application/created DateTime
    :application/modified DateTime
    (s/optional-key :application/first-submitted) DateTime
    (s/optional-key :application/copied-from) {:application/id s/Int
-                                              :application/external-id (s/maybe s/Str)}
+                                              :application/external-id s/Str}
    (s/optional-key :application/copied-to) [{:application/id s/Int
-                                             :application/external-id (s/maybe s/Str)}]
+                                             :application/external-id s/Str}]
    :application/last-activity DateTime
    :application/applicant s/Str
    :application/applicant-attributes {s/Keyword s/Str}
diff --git a/src/clj/rems/application/commands.clj b/src/clj/rems/application/commands.clj
index 08791255f..d585cf1ee 100644
--- a/src/clj/rems/application/commands.clj
+++ b/src/clj/rems/application/commands.clj
@@ -23,6 +23,9 @@
 (s/defschema CommandBase
   {:application-id s/Int})
 
+(s/defschema CreateCommand
+  {:catalogue-item-ids [s/Int]})
+
 (s/defschema SaveDraftCommand
   (assoc CommandBase
          ;; {s/Int s/Str} is what we want, but that isn't nicely representable as JSON
@@ -115,28 +118,27 @@
   CommandBase)
 
 (def command-schemas
-  {#_:application.command/require-license
-   :application.command/accept-invitation AcceptInvitationCommand
+  {:application.command/accept-invitation AcceptInvitationCommand
    :application.command/accept-licenses AcceptLicensesCommand
    :application.command/add-licenses AddLicensesCommand
    :application.command/add-member AddMemberCommand
-   :application.command/change-resources ChangeResourcesCommand
-   :application.command/invite-member InviteMemberCommand
    :application.command/approve ApproveCommand
+   :application.command/change-resources ChangeResourcesCommand
    :application.command/close CloseCommand
    :application.command/comment CommentCommand
+   :application.command/copy-as-new CopyAsNewCommand
+   :application.command/create CreateCommand
    :application.command/decide DecideCommand
-   :application.command/remark RemarkCommand
+   :application.command/invite-member InviteMemberCommand
    :application.command/reject RejectCommand
+   :application.command/remark RemarkCommand
+   :application.command/remove-member RemoveMemberCommand
    :application.command/request-comment RequestCommentCommand
    :application.command/request-decision RequestDecisionCommand
-   :application.command/remove-member RemoveMemberCommand
    :application.command/return ReturnCommand
    :application.command/save-draft SaveDraftCommand
    :application.command/submit SubmitCommand
-   :application.command/uninvite-member UninviteMemberCommand
-   :application.command/copy-as-new CopyAsNewCommand
-   #_:application.command/withdraw})
+   :application.command/uninvite-member UninviteMemberCommand})
 
 (s/defschema Command
   (merge (apply r/StructDispatch :type (flatten (seq command-schemas)))
@@ -177,20 +179,24 @@
   (is (= (set (keys command-schemas))
          (set (keys (methods command-handler))))))
 
-(defn- invalid-user-error [user-id injections]
+(defn- must-not-be-empty [cmd key]
+  (when-not (seq (get cmd key))
+    {:errors [{:type :must-not-be-empty :key key}]}))
+
+(defn- invalid-user-error [user-id {:keys [valid-user?]}]
   (cond
-    (not (:valid-user? injections)) {:errors [{:type :missing-injection :injection :valid-user?}]}
-    (not ((:valid-user? injections) user-id)) {:errors [{:type :t.form.validation/invalid-user :userid user-id}]}))
+    (not valid-user?) {:errors [{:type :missing-injection :injection :valid-user?}]}
+    (not (valid-user? user-id)) {:errors [{:type :t.form.validation/invalid-user :userid user-id}]}))
 
 (defn- invalid-users-errors
   "Checks the given users for validity and merges the errors"
   [user-ids injections]
   (apply merge-with into (keep #(invalid-user-error % injections) user-ids)))
 
-(defn- invalid-catalogue-item-error [catalogue-item-id injections]
+(defn- invalid-catalogue-item-error [catalogue-item-id {:keys [get-catalogue-item]}]
   (cond
-    (not (:get-catalogue-item injections)) {:errors [{:type :missing-injection :injection :get-catalogue-item}]}
-    (not ((:get-catalogue-item injections) catalogue-item-id)) {:errors [{:type :invalid-catalogue-item :catalogue-item-id catalogue-item-id}]}))
+    (not get-catalogue-item) {:errors [{:type :missing-injection :injection :get-catalogue-item}]}
+    (not (get-catalogue-item catalogue-item-id)) {:errors [{:type :invalid-catalogue-item :catalogue-item-id catalogue-item-id}]}))
 
 (defn- invalid-catalogue-items
   "Checks the given catalogue items for validity and merges the errors"
@@ -199,8 +205,8 @@
 
 (defn- changes-original-workflow
   "Checks that the given catalogue items are compatible with the original application from where the workflow is from. Applicant can't do it."
-  [application catalogue-item-ids actor injections]
-  (let [catalogue-items (map (:get-catalogue-item injections) catalogue-item-ids)
+  [application catalogue-item-ids actor {:keys [get-catalogue-item]}]
+  (let [catalogue-items (map get-catalogue-item catalogue-item-ids)
         original-workflow-id (get-in application [:application/workflow :workflow/id])
         new-workflow-ids (mapv :wfid catalogue-items)
         handlers (get-in application [:application/workflow :workflow.dynamic/handlers])]
@@ -210,8 +216,8 @@
 
 (defn- changes-original-form
   "Checks that the given catalogue items are compatible with the original application from where the form is from. Applicant can't do it."
-  [application catalogue-item-ids actor injections]
-  (let [catalogue-items (map (:get-catalogue-item injections) catalogue-item-ids)
+  [application catalogue-item-ids actor {:keys [get-catalogue-item]}]
+  (let [catalogue-items (map get-catalogue-item catalogue-item-ids)
         original-form-id (get-in application [:application/form :form/id])
         new-form-ids (mapv :formid catalogue-items)
         handlers (get-in application [:application/workflow :workflow.dynamic/handlers])]
@@ -220,15 +226,20 @@
       {:errors [{:type :changes-original-form :form/id original-form-id :ids new-form-ids}]})))
 
 (defn- unbundlable-catalogue-items
+  "Checks that the given catalogue items are bundlable."
+  [catalogue-item-ids {:keys [get-catalogue-item]}]
+  (let [catalogue-items (map get-catalogue-item catalogue-item-ids)]
+    (when (not= 1
+                (count (set (map :formid catalogue-items)))
+                (count (set (map :wfid catalogue-items))))
+      {:errors [{:type :unbundlable-catalogue-items :catalogue-item-ids catalogue-item-ids}]})))
+
+(defn- unbundlable-catalogue-items-for-actor
   "Checks that the given catalogue items are bundlable by the given actor."
   [application catalogue-item-ids actor injections]
-  (let [catalogue-items (map (:get-catalogue-item injections) catalogue-item-ids)
-        handlers (get-in application [:application/workflow :workflow.dynamic/handlers])]
-    (when (and (not (contains? handlers actor))
-               (not= 1
-                     (count (set (map :formid catalogue-items)))
-                     (count (set (map :wfid catalogue-items)))))
-      {:errors [{:type :unbundlable-catalogue-items :catalogue-item-ids catalogue-item-ids}]})))
+  (let [handlers (get-in application [:application/workflow :workflow.dynamic/handlers])]
+    (when-not (contains? handlers actor)
+      (unbundlable-catalogue-items catalogue-item-ids injections))))
 
 (defn- validation-error [application {:keys [validate-fields]}]
   (let [errors (validate-fields (getx-in application [:application/form :form/fields]))]
@@ -256,6 +267,56 @@
 (defn- ok [& events]
   (ok-with-data nil events))
 
+(defn- build-resources-list [catalogue-item-ids {:keys [get-catalogue-item]}]
+  (->> catalogue-item-ids
+       (mapv get-catalogue-item)
+       (mapv (fn [catalogue-item]
+               {:catalogue-item/id (:id catalogue-item)
+                :resource/ext-id (:resid catalogue-item)}))))
+
+(defn- build-licenses-list [catalogue-item-ids {:keys [get-catalogue-item-licenses]}]
+  (->> catalogue-item-ids
+       (mapcat get-catalogue-item-licenses)
+       distinct
+       (mapv (fn [license]
+               {:license/id (:id license)}))))
+
+(defn- application-created-event! [{:keys [catalogue-item-ids time actor] :as cmd}
+                                   {:keys [allocate-application-ids! get-catalogue-item get-workflow]
+                                    :as injections}]
+  (or (must-not-be-empty cmd :catalogue-item-ids)
+      (invalid-catalogue-items catalogue-item-ids injections)
+      (unbundlable-catalogue-items catalogue-item-ids injections)
+      (let [items (map get-catalogue-item catalogue-item-ids)
+            form-id (:formid (first items))
+            workflow-id (:wfid (first items))
+            workflow-type (:type (:workflow (get-workflow workflow-id)))
+            _ (assert (= :workflow/dynamic workflow-type) {:workflow-type workflow-type}) ; TODO: support other workflows
+            ids (allocate-application-ids! time)]
+        {:event {:event/type :application.event/created
+                 :event/time time
+                 :event/actor actor
+                 :application/id (:application/id ids)
+                 :application/external-id (:application/external-id ids)
+                 :application/resources (build-resources-list catalogue-item-ids injections)
+                 :application/licenses (build-licenses-list catalogue-item-ids injections)
+                 :form/id form-id
+                 :workflow/id workflow-id
+                 :workflow/type workflow-type}})))
+
+(defmethod command-handler :application.command/create
+  [cmd application injections]
+  ;; XXX: handle-command will execute this method even when the permission for it is missing,
+  ;;      so we need to guard against that to avoid the mutative operation of allocating
+  ;;      new application IDs when the application already exists.
+  (when (nil? application)
+    (let [created-event-or-errors (application-created-event! cmd injections)]
+      (if (:errors created-event-or-errors)
+        created-event-or-errors
+        (let [event (:event created-event-or-errors)]
+          (ok-with-data {:application-id (:application/id event)}
+                        [event]))))))
+
 (defmethod command-handler :application.command/save-draft
   [cmd _application _injections]
   (ok {:event/type :application.event/draft-saved
@@ -293,10 +354,6 @@
   (ok {:event/type :application.event/closed
        :application/comment (:comment cmd)}))
 
-(defn- must-not-be-empty [cmd key]
-  (when-not (seq (get cmd key))
-    {:errors [{:type :must-not-be-empty :key key}]}))
-
 (defmethod command-handler :application.command/request-decision
   [cmd _application injections]
   (or (must-not-be-empty cmd :deciders)
@@ -364,20 +421,12 @@
   [cmd application injections]
   (or (must-not-be-empty cmd :catalogue-item-ids)
       (invalid-catalogue-items (:catalogue-item-ids cmd) injections)
-      (unbundlable-catalogue-items application (:catalogue-item-ids cmd) (:actor cmd) injections)
+      (unbundlable-catalogue-items-for-actor application (:catalogue-item-ids cmd) (:actor cmd) injections)
       (changes-original-form application (:catalogue-item-ids cmd) (:actor cmd) injections)
       (changes-original-workflow application (:catalogue-item-ids cmd) (:actor cmd) injections)
       (ok (merge {:event/type :application.event/resources-changed
-                  :application/resources (->> (:catalogue-item-ids cmd)
-                                              (mapv (:get-catalogue-item injections))
-                                              (mapv (fn [catalogue-item]
-                                                      {:catalogue-item/id (:id catalogue-item)
-                                                       :resource/ext-id (:resid catalogue-item)})))
-                  :application/licenses (->> (:catalogue-item-ids cmd)
-                                             (mapcat (:get-catalogue-item-licenses injections))
-                                             distinct
-                                             (mapv (fn [license]
-                                                     {:license/id (:id license)})))}
+                  :application/resources (build-resources-list (:catalogue-item-ids cmd) injections)
+                  :application/licenses (build-licenses-list (:catalogue-item-ids cmd) injections)}
                  (when (:comment cmd)
                    {:application/comment (:comment cmd)})))))
 
@@ -389,13 +438,13 @@
            :application/member (:member cmd)})))
 
 (defmethod command-handler :application.command/invite-member
-  [cmd _application injections]
+  [cmd _application {:keys [secure-token]}]
   (ok {:event/type :application.event/member-invited
        :application/member (:member cmd)
-       :invitation/token ((getx injections :secure-token))}))
+       :invitation/token (secure-token)}))
 
 (defmethod command-handler :application.command/accept-invitation
-  [cmd application injections]
+  [cmd application _injections]
   (or (already-member-error application (:actor cmd))
       (invitation-token-error application (:token cmd))
       (ok-with-data {:application-id (:application-id cmd)}
@@ -425,28 +474,32 @@
            :application/comment (:comment cmd)})))
 
 (defmethod command-handler :application.command/copy-as-new
-  [cmd application {:keys [application-created-event]}]
+  [cmd application injections]
   (let [catalogue-item-ids (map :catalogue-item/id (:application/resources application))
-        created-event (application-created-event {:catalogue-item-ids catalogue-item-ids
-                                                  :time (:time cmd)
-                                                  :actor (:actor cmd)})
-        old-app-id (:application/id application)
-        new-app-id (:application/id created-event)]
-    (ok-with-data
-     {:application-id new-app-id}
-     [created-event
-      {:event/type :application.event/draft-saved
-       :application/id new-app-id
-       :application/field-values (->> (:form/fields (:application/form application))
-                                      (map (fn [field]
-                                             [(:field/id field) (:field/value field)]))
-                                      (into {}))}
-      {:event/type :application.event/copied-from
-       :application/id new-app-id
-       :application/copied-from (select-keys application [:application/id :application/external-id])}
-      {:event/type :application.event/copied-to
-       :application/id old-app-id
-       :application/copied-to (select-keys created-event [:application/id :application/external-id])}])))
+        created-event-or-errors (application-created-event! {:catalogue-item-ids catalogue-item-ids
+                                                             :time (:time cmd)
+                                                             :actor (:actor cmd)}
+                                                            injections)]
+    (if (:errors created-event-or-errors)
+      created-event-or-errors
+      (let [created-event (:event created-event-or-errors)
+            old-app-id (:application/id application)
+            new-app-id (:application/id created-event)]
+        (ok-with-data
+         {:application-id new-app-id}
+         [created-event
+          {:event/type :application.event/draft-saved
+           :application/id new-app-id
+           :application/field-values (->> (:form/fields (:application/form application))
+                                          (map (fn [field]
+                                                 [(:field/id field) (:field/value field)]))
+                                          (into {}))}
+          {:event/type :application.event/copied-from
+           :application/id new-app-id
+           :application/copied-from (select-keys application [:application/id :application/external-id])}
+          {:event/type :application.event/copied-to
+           :application/id old-app-id
+           :application/copied-to (select-keys created-event [:application/id :application/external-id])}])))))
 
 (defn- add-common-event-fields-from-command [event cmd]
   (-> event
@@ -466,7 +519,9 @@
 
 (defn handle-command [cmd application injections]
   (validate-command cmd) ; this is here mostly for tests, commands via the api are validated by compojure-api
-  (let [permissions (permissions/user-permissions application (:actor cmd))]
+  (let [permissions (if application
+                      (permissions/user-permissions application (:actor cmd))
+                      #{:application.command/create})]
     (if (contains? permissions (:type cmd))
       (-> (command-handler cmd application injections)
           (enrich-result cmd)
diff --git a/src/clj/rems/application/events.clj b/src/clj/rems/application/events.clj
index 44b589967..42e874eb5 100644
--- a/src/clj/rems/application/events.clj
+++ b/src/clj/rems/application/events.clj
@@ -41,7 +41,7 @@
 (s/defschema CreatedEvent
   (assoc EventBase
          :event/type (s/enum :application.event/created)
-         :application/external-id (s/maybe s/Str)
+         :application/external-id s/Str
          :application/resources [{:catalogue-item/id s/Int
                                   :resource/ext-id s/Str}]
          :application/licenses [{:license/id s/Int}]
@@ -125,12 +125,12 @@
   (assoc EventBase
          :event/type (s/enum :application.event/copied-from)
          :application/copied-from {:application/id s/Int
-                                   :application/external-id (s/maybe s/Str)}))
+                                   :application/external-id s/Str}))
 (s/defschema CopiedToEvent
   (assoc EventBase
          :event/type (s/enum :application.event/copied-to)
          :application/copied-to {:application/id s/Int
-                                 :application/external-id (s/maybe s/Str)}))
+                                 :application/external-id s/Str}))
 
 (def event-schemas
   {:application.event/approved ApprovedEvent
diff --git a/src/clj/rems/db/applications.clj b/src/clj/rems/db/applications.clj
index 698be5429..4e0b14243 100644
--- a/src/clj/rems/db/applications.clj
+++ b/src/clj/rems/db/applications.clj
@@ -26,9 +26,18 @@
             [rems.util :refer [atom? getx secure-token]])
   (:import [org.joda.time Duration]))
 
+;;; Query functions
+
+;; TODO use also in UI side?
+(defn is-dynamic-application? [application]
+  (= :workflow/dynamic (get-in application [:workflow :type])))
+
+(defn is-dynamic-handler? [user-id application]
+  (contains? (set (get-in application [:workflow :handlers])) user-id))
+
 ;;; Creating applications
 
-(defn allocate-external-id! [prefix]
+(defn- allocate-external-id! [prefix]
   (conman/with-transaction [rems.db.core/*db* {:isolation :serializable}]
     (let [all (db/get-external-ids {:prefix prefix})
           last (apply max (cons 0 (map (comp read-string :suffix) all)))
@@ -36,57 +45,20 @@
       (db/add-external-id! {:prefix prefix :suffix new})
       {:prefix prefix :suffix new})))
 
-(defn format-external-id [{:keys [prefix suffix]}]
+(defn- format-external-id [{:keys [prefix suffix]}]
   (str prefix "/" suffix))
 
-(defn application-external-id! [time]
+(defn- application-external-id! [time]
   (let [id-prefix (str (.getYear time))]
     (format-external-id (allocate-external-id! id-prefix))))
 
-(defn application-created-event [{:keys [application-id catalogue-item-ids time actor allocate-external-id?]
-                                  :or {allocate-external-id? true}}]
-  (assert (seq catalogue-item-ids) "catalogue item not specified")
-  (let [application-id (or application-id (:id (db/create-application!)))
-        items (catalogue/get-localized-catalogue-items {:ids catalogue-item-ids})]
-    (assert (= (count items) (count catalogue-item-ids)) "catalogue item not found")
-    (assert (= 1 (count (distinct (mapv :wfid items)))) "catalogue items did not have the same workflow")
-    (assert (= 1 (count (distinct (mapv :formid items)))) "catalogue items did not have the same form")
-    (let [workflow-id (:wfid (first items))
-          form-id (:formid (first items))
-          workflow (-> (:workflow (workflow/get-workflow workflow-id))
-                       (update :type keyword))
-          licenses (db/get-licenses {:wfid workflow-id
-                                     :items catalogue-item-ids})]
-      (assert (= :workflow/dynamic (:type workflow))
-              (str "workflow type was " (:type workflow))) ; TODO: support other workflows
-      {:event/type :application.event/created
-       :event/time time
-       :event/actor actor
-       :application/id application-id
-       :application/external-id (when allocate-external-id? ;; TODO parameterize id allocation?
-                                  (application-external-id! time))
-       :application/resources (map (fn [item]
-                                     {:catalogue-item/id (:id item)
-                                      :resource/ext-id (:resid item)})
-                                   items)
-       :application/licenses (map (fn [license]
-                                    {:license/id (:id license)})
-                                  licenses)
-       :form/id form-id
-       :workflow/id workflow-id
-       :workflow/type (:type workflow)})))
-
-(defn create-application! [user-id catalogue-item-ids]
-  (let [event (application-created-event {:catalogue-item-ids catalogue-item-ids
-                                          :time (time/now)
-                                          :actor user-id})]
-    (events/add-event! event)
-    {:success true
-     :application-id (:application/id event)}))
+(defn- allocate-application-ids! [time]
+  {:application/id (:id (db/create-application!))
+   :application/external-id (application-external-id! time)})
 
 ;;; Running commands
 
-(defn- get-catalogue-item-licenses [catalogue-item-id]
+(defn get-catalogue-item-licenses [catalogue-item-id]
   (db/get-licenses
    {:wfid (:wfid (catalogue/get-localized-catalogue-item catalogue-item-id))
     :items [catalogue-item-id]}))
@@ -94,25 +66,25 @@
 (defn- valid-user? [userid]
   (not (nil? (users/get-user-attributes userid))))
 
-(def ^:private db-injections
+(def ^:private command-injections
   {:valid-user? valid-user?
    :validate-fields form-validation/validate-fields
    :secure-token secure-token
    :get-catalogue-item catalogue/get-localized-catalogue-item
    :get-catalogue-item-licenses get-catalogue-item-licenses
-   :application-created-event application-created-event})
+   :get-workflow workflow/get-workflow
+   :allocate-application-ids! allocate-application-ids!})
 
 (declare get-unrestricted-application)
-
 (defn command! [cmd]
-  (assert (:application-id cmd))
   ;; Use locks to prevent multiple commands being executed in parallel.
   ;; Serializable isolation level will already avoid anomalies, but produces
   ;; lots of transaction conflicts when there is contention. This lock
   ;; roughly doubles the throughput for rems.db.test-transactions tests.
   (jdbc/execute! db/*db* ["LOCK TABLE application_event IN SHARE ROW EXCLUSIVE MODE"])
-  (let [app (get-unrestricted-application (:application-id cmd))
-        result (commands/handle-command cmd app db-injections)]
+  (let [app (when-let [app-id (:application-id cmd)]
+              (get-unrestricted-application app-id))
+        result (commands/handle-command cmd app command-injections)]
     (if (not (:errors result))
       (doseq [event (:events result)]
         (events/add-event! event)))
@@ -123,7 +95,7 @@
 
 ;;; Fetching applications (for API) (incl. caching)
 
-(def ^:private injections
+(def ^:private fetcher-injections
   {:get-attachments-for-application attachments/get-attachments-for-application
    :get-form-template form/get-form-template
    :get-catalogue-item catalogue/get-localized-catalogue-item
@@ -143,7 +115,7 @@
   [application-id]
   (let [events (events/get-application-events application-id)
         cache-key [application-id (count events)]
-        build-app (fn [_] (model/build-application-view events injections))]
+        build-app (fn [_] (model/build-application-view events fetcher-injections))]
     (if (empty? events)
       nil ; application not found
       ;; TODO: this caching could be removed by refactoring the pollers to build their own projection
@@ -244,7 +216,7 @@
            ;; TODO: batched injections: only one DB query to fetch all catalogue items etc.
            ;;       - fetch all items in the background as a batch, use plain maps as injections
            ;;       - change db/get-license and db/get-user-attributes to fetch all rows if ID is not defined
-           cached-injections (map-vals memoize injections)
+           cached-injections (map-vals memoize fetcher-injections)
            enriched-apps (->> (select-keys raw-apps updated-app-ids)
                               (map-vals #(model/enrich-with-injections % cached-injections))
                               (merge (::enriched-apps state)))]
diff --git a/src/clj/rems/db/licenses.clj b/src/clj/rems/db/licenses.clj
index 2f57b6fbe..601da4524 100644
--- a/src/clj/rems/db/licenses.clj
+++ b/src/clj/rems/db/licenses.clj
@@ -79,3 +79,10 @@
        (format-licenses)
        (localize-licenses)
        (distinct-by :id)))
+
+(defn get-active-licenses
+  "Get licenses active now. Params map similar to get-licenses."
+  [now params]
+  (->> (get-licenses params)
+       (map (partial db/assoc-expired now))
+       (remove :expired)))
diff --git a/src/clj/rems/db/workflow.clj b/src/clj/rems/db/workflow.clj
index 60f262b99..d9319f50f 100644
--- a/src/clj/rems/db/workflow.clj
+++ b/src/clj/rems/db/workflow.clj
@@ -26,6 +26,7 @@
       (update :workflow json/parse-string)
       (assoc :licenses (get-workflow-licenses (:id wf)))
       db/assoc-expired
+      (update-in [:workflow :type] keyword)
       (update-in [:workflow :handlers] #(mapv users/get-user %))))
 
 (defn get-workflow [id]


Test patch:
diff --git a/src/clj/rems/db/test_data.clj b/src/clj/rems/db/test_data.clj
index 7f54006b9..96cdead26 100644
--- a/src/clj/rems/db/test_data.clj
+++ b/src/clj/rems/db/test_data.clj
@@ -63,7 +63,8 @@
                        command)
         result (applications/command! command)]
     (assert (not (:errors result))
-            {:command command :result result})))
+            {:command command :result result})
+    result))
 
 (defn- transpose-localizations [m]
   (->> m
@@ -143,12 +144,10 @@
           (assert (:success result) {:command [id lang text] :result result}))))
     id))
 
-(defn create-application! [{:keys [catalogue-item-ids actor]
-                            :as command}]
-  (let [result (applications/create-application! actor
-                                                 (or catalogue-item-ids [(create-catalogue-item! {})]))]
-    (assert (:success result) {:command command :result result})
-    (:application-id result)))
+(defn create-application! [{:keys [catalogue-item-ids actor]}]
+  (:application-id (command! {:type :application.command/create
+                              :catalogue-item-ids (or catalogue-item-ids [(create-catalogue-item! {})])
+                              :actor actor})))
 
 (defn- base-command [{:keys [application-id actor time]}]
   (assert application-id)
diff --git a/test/clj/rems/application/test_commands.clj b/test/clj/rems/application/test_commands.clj
index de7f0a94b..0fbcb7b09 100644
--- a/test/clj/rems/application/test_commands.clj
+++ b/test/clj/rems/application/test_commands.clj
@@ -10,15 +10,14 @@
            [org.joda.time DateTime]))
 
 (def ^:private test-time (DateTime. 1000))
-(def ^:private command-defaults {:application-id 123
-                                 :time test-time})
+(def ^:private app-id 123)
 (def ^:private applicant-user-id "applicant")
 (def ^:private handler-user-id "assistant")
 (def ^:private dummy-created-event {:event/type :application.event/created
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123
-                                    :application/external-id nil
+                                    :application/id app-id
+                                    :application/external-id "2000/123"
                                     :application/resources []
                                     :application/licenses []
                                     :form/id 1
@@ -36,8 +35,7 @@
    :get-license (constantly nil)
    :get-user (constantly nil)
    :get-users-with-role (constantly nil)
-   :get-attachments-for-application (constantly nil)
-   :application-created-event (constantly nil)})
+   :get-attachments-for-application (constantly nil)})
 
 ;; could rework tests to use model/build-application-view instead of this
 (defn apply-events [application events]
@@ -45,11 +43,17 @@
   (-> (reduce model/application-view application events)
       (model/enrich-with-injections injections)))
 
+(defn- set-command-defaults [cmd]
+  (-> cmd
+      (assoc :time test-time)
+      (cond-> (not= :application.command/create (:type cmd))
+              (assoc :application-id app-id))))
+
 (defn- fail-command
   ([application cmd]
    (fail-command application cmd nil))
   ([application cmd injections]
-   (let [cmd (merge command-defaults cmd)
+   (let [cmd (set-command-defaults cmd)
          result (commands/handle-command cmd application injections)]
      (assert-ex (:errors result) {:cmd cmd :result result})
      result)))
@@ -58,7 +62,7 @@
   ([application cmd]
    (ok-command application cmd nil))
   ([application cmd injections]
-   (let [cmd (merge command-defaults cmd)
+   (let [cmd (set-command-defaults cmd)
          result (commands/handle-command cmd application injections)]
      (assert-ex (not (:errors result)) {:cmd cmd :result result})
      (let [events (:events result)]
@@ -83,13 +87,118 @@
 
 ;;; Tests
 
+(deftest test-create
+  (let [cat-id 10
+        cat-id2 20
+        licence-id 30
+        licence-id2 40
+        form-id 50
+        wf-id 60
+        injections {:get-catalogue-item {cat-id {:id cat-id
+                                                 :resid "res1"
+                                                 :formid form-id
+                                                 :wfid wf-id}
+                                         cat-id2 {:id cat-id2
+                                                  :resid "res2"
+                                                  :formid form-id
+                                                  :wfid wf-id}}
+                    :get-catalogue-item-licenses {cat-id [{:id licence-id}]
+                                                  cat-id2 [{:id licence-id2}]}
+                    :get-workflow {wf-id {:workflow {:type :workflow/dynamic}}}
+                    :allocate-application-ids! (fn [^DateTime time]
+                                                 {:application/id app-id
+                                                  :application/external-id (str (.getYear time) "/1")})}]
+    (testing "one resource"
+      (is (= {:event/type :application.event/created
+              :event/actor applicant-user-id
+              :event/time (DateTime. 1000)
+              :application/id app-id
+              :application/external-id "1970/1"
+              :application/resources [{:catalogue-item/id cat-id
+                                       :resource/ext-id "res1"}]
+              :application/licenses [{:license/id licence-id}]
+              :form/id form-id
+              :workflow/id wf-id
+              :workflow/type :workflow/dynamic}
+             (ok-command nil {:type :application.command/create
+                              :actor applicant-user-id
+                              :catalogue-item-ids [cat-id]}
+                         injections))))
+
+    (testing "multiple resources"
+      (is (= {:event/type :application.event/created
+              :event/actor applicant-user-id
+              :event/time (DateTime. 1000)
+              :application/id app-id
+              :application/external-id "1970/1"
+              :application/resources [{:catalogue-item/id cat-id
+                                       :resource/ext-id "res1"}
+                                      {:catalogue-item/id cat-id2
+                                       :resource/ext-id "res2"}]
+              :application/licenses [{:license/id licence-id}
+                                     {:license/id licence-id2}]
+              :form/id form-id
+              :workflow/id wf-id
+              :workflow/type :workflow/dynamic}
+             (ok-command nil {:type :application.command/create
+                              :actor applicant-user-id
+                              :catalogue-item-ids [cat-id cat-id2]}
+                         injections))))
+
+    (testing "error: zero catalogue items"
+      (is (= {:errors [{:type :must-not-be-empty
+                        :key :catalogue-item-ids}]}
+             (fail-command nil {:type :application.command/create
+                                :actor applicant-user-id
+                                :catalogue-item-ids []}
+                           injections))))
+
+    (testing "error: non-existing catalogue items"
+      (is (= {:errors [{:type :invalid-catalogue-item
+                        :catalogue-item-id 999999}]}
+             (fail-command nil {:type :application.command/create
+                                :actor applicant-user-id
+                                :catalogue-item-ids [999999]}
+                           injections))))
+
+    (testing "error: catalogue items with different forms"
+      (let [injections (assoc-in injections [:get-catalogue-item cat-id2 :formid] 666)]
+        (is (= {:errors [{:type :unbundlable-catalogue-items
+                          :catalogue-item-ids [10 20]}]}
+               (fail-command nil {:type :application.command/create
+                                  :actor applicant-user-id
+                                  :catalogue-item-ids [cat-id cat-id2]}
+                             injections)))))
+
+    (testing "error: catalogue items with different workflows"
+      (let [injections (assoc-in injections [:get-catalogue-item cat-id2 :wfid] 666)]
+        (is (= {:errors [{:type :unbundlable-catalogue-items
+                          :catalogue-item-ids [10 20]}]}
+               (fail-command nil {:type :application.command/create
+                                  :actor applicant-user-id
+                                  :catalogue-item-ids [cat-id cat-id2]}
+                             injections)))))
+
+    (testing "cannot execute the create command for an existing application"
+      (let [allocated-new-ids? (atom false)
+            injections (assoc injections :allocate-application-ids! (fn [_]
+                                                                      (reset! allocated-new-ids? true)))
+            application (apply-events nil [dummy-created-event])]
+        (is (= {:errors [{:type :forbidden}]}
+               (fail-command application {:type :application.command/create
+                                          :actor applicant-user-id
+                                          :catalogue-item-ids [cat-id]}
+                             injections)))
+        (is (false? @allocated-new-ids?)
+            "should not allocate new IDs")))))
+
 (deftest test-save-draft
   (let [application (apply-events nil [dummy-created-event])]
     (testing "saves a draft"
       (is (= {:event/type :application.event/draft-saved
               :event/time test-time
               :event/actor applicant-user-id
-              :application/id 123
+              :application/id app-id
               :application/field-values {1 "foo" 2 "bar"}}
              (ok-command application
                          {:type :application.command/save-draft
@@ -113,7 +222,7 @@
                                       [{:event/type :application.event/submitted
                                         :event/time test-time
                                         :event/actor applicant-user-id
-                                        :application/id 123}])]
+                                        :application/id app-id}])]
         (is (= {:errors [{:type :forbidden}]}
                (fail-command application
                              {:type :application.command/save-draft
@@ -124,16 +233,16 @@
                                       [{:event/type :application.event/submitted
                                         :event/time test-time
                                         :event/actor applicant-user-id
-                                        :application/id 123}
+                                        :application/id app-id}
                                        {:event/type :application.event/returned
                                         :event/time test-time
                                         :event/actor handler-user-id
-                                        :application/id 123
+                                        :application/id app-id
                                         :application/comment ""}])]
         (is (= {:event/type :application.event/draft-saved
                 :event/time test-time
                 :event/actor applicant-user-id
-                :application/id 123
+                :application/id app-id
                 :application/field-values {1 "updated"}}
                (ok-command application
                            {:type :application.command/save-draft
@@ -145,7 +254,7 @@
     (is (= {:event/type :application.event/licenses-accepted
             :event/time test-time
             :event/actor applicant-user-id
-            :application/id 123
+            :application/id app-id
             :application/accepted-licenses #{1 2}}
            (ok-command application
                        {:type :application.command/accept-licenses
@@ -157,11 +266,11 @@
                                        {:event/type :application.event/submitted
                                         :event/time test-time
                                         :event/actor applicant-user-id
-                                        :application/id 123}])]
+                                        :application/id app-id}])]
     (is (= {:event/type :application.event/licenses-added
             :event/time test-time
             :event/actor handler-user-id
-            :application/id 123
+            :application/id app-id
             :application/comment "comment"
             :application/licenses [{:license/id 1} {:license/id 2}]}
            (ok-command application
@@ -192,13 +301,13 @@
                                             [{:event/type :application.event/submitted
                                               :event/time test-time
                                               :event/actor applicant-user-id
-                                              :application/id 123}])
+                                              :application/id app-id}])
         approved-application (apply-events submitted-application
                                            [{:event/type :application.event/approved
                                              :event/time test-time
                                              :event/actor handler-user-id
                                              :application/comment "This is good"
-                                             :application/id 123}])
+                                             :application/id app-id}])
         injections {:get-catalogue-item
                     {cat-1 {:id cat-1 :resid "res1" :formid form-1 :wfid wf-1}
                      cat-2-other-license {:id cat-2-other-license :resid "res2" :formid form-1 :wfid wf-1}
@@ -214,7 +323,7 @@
       (is (= {:event/type :application.event/resources-changed
               :event/time test-time
               :event/actor applicant-user-id
-              :application/id 123
+              :application/id app-id
               :application/resources [{:catalogue-item/id cat-1 :resource/ext-id "res1"}
                                       {:catalogue-item/id cat-2-other-license :resource/ext-id "res2"}]
               :application/licenses [{:license/id license-1}
@@ -255,7 +364,7 @@
       (is (= {:event/type :application.event/resources-changed
               :event/time test-time
               :event/actor applicant-user-id
-              :application/id 123
+              :application/id app-id
               :application/resources [{:catalogue-item/id cat-2-other-license :resource/ext-id "res2"}]
               :application/licenses [{:license/id license-2}]}
              (ok-command application
@@ -284,7 +393,7 @@
       (is (= {:event/type :application.event/resources-changed
               :event/time test-time
               :event/actor handler-user-id
-              :application/id 123
+              :application/id app-id
               :application/comment "Changed these for you"
               :application/resources [{:catalogue-item/id cat-1 :resource/ext-id "res1"}
                                       {:catalogue-item/id cat-2-other-license :resource/ext-id "res2"}]
@@ -301,7 +410,7 @@
         (is (= {:event/type :application.event/resources-changed
                 :event/time test-time
                 :event/actor handler-user-id
-                :application/id 123
+                :application/id app-id
                 :application/comment "Changed these for you"
                 :application/resources [{:catalogue-item/id cat-1 :resource/ext-id "res1"}
                                         {:catalogue-item/id cat-3-other-workflow :resource/ext-id "res3"}
@@ -318,7 +427,7 @@
       (is (= {:event/type :application.event/resources-changed
               :event/time test-time
               :event/actor handler-user-id
-              :application/id 123
+              :application/id app-id
               :application/comment "Changed these for you"
               :application/resources [{:catalogue-item/id cat-1 :resource/ext-id "res1"}
                                       {:catalogue-item/id cat-2-other-license :resource/ext-id "res2"}]
@@ -335,7 +444,7 @@
         (is (= {:event/type :application.event/resources-changed
                 :event/time test-time
                 :event/actor handler-user-id
-                :application/id 123
+                :application/id app-id
                 :application/comment "Changed these for you"
                 :application/resources [{:catalogue-item/id cat-1 :resource/ext-id "res1"}
                                         {:catalogue-item/id cat-3-other-workflow :resource/ext-id "res3"}
@@ -368,8 +477,8 @@
         created-event {:event/type :application.event/created
                        :event/time test-time
                        :event/actor applicant-user-id
-                       :application/id 123
-                       :application/external-id nil
+                       :application/id app-id
+                       :application/external-id "2000/123"
                        :application/resources [{:catalogue-item/id 10
                                                 :resource/ext-id "urn:11"}
                                                {:catalogue-item/id 20
@@ -381,7 +490,7 @@
         draft-saved-event {:event/type :application.event/draft-saved
                            :event/time test-time
                            :event/actor applicant-user-id
-                           :application/id 123
+                           :application/id app-id
                            :application/field-values {41 "foo"
                                                       42 "bar"}}
         submit-command {:type :application.command/submit
@@ -392,7 +501,7 @@
       (is (= {:event/type :application.event/submitted
               :event/time test-time
               :event/actor applicant-user-id
-              :application/id 123}
+              :application/id app-id}
              (ok-command application submit-command injections))))
 
     (testing "cannot submit when required fields are empty"
@@ -414,7 +523,7 @@
                  (apply-events [{:event/type :application.event/submitted
                                  :event/time test-time
                                  :event/actor applicant-user-id
-                                 :application/id 123}])
+                                 :application/id app-id}])
                  (fail-command submit-command injections)))))))
 
 (deftest test-approve-or-reject
@@ -423,12 +532,12 @@
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}])]
+                                    :application/id app-id}])]
     (testing "approved successfully"
       (is (= {:event/type :application.event/approved
               :event/time test-time
               :event/actor handler-user-id
-              :application/id 123
+              :application/id app-id
               :application/comment "fine"}
              (ok-command application
                          {:type :application.command/approve
@@ -439,7 +548,7 @@
               :application/comment "bad"
               :event/time test-time
               :event/actor handler-user-id
-              :application/id 123}
+              :application/id app-id}
              (ok-command application
                          {:type :application.command/reject
                           :actor handler-user-id
@@ -451,11 +560,11 @@
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}])]
+                                    :application/id app-id}])]
     (is (= {:event/type :application.event/returned
             :event/time test-time
             :event/actor handler-user-id
-            :application/id 123
+            :application/id app-id
             :application/comment "ret"}
            (ok-command application
                        {:type :application.command/return
@@ -468,16 +577,16 @@
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}
+                                    :application/id app-id}
                                    {:event/type :application.event/approved
                                     :event/time test-time
                                     :event/actor handler-user-id
-                                    :application/id 123
+                                    :application/id app-id
                                     :application/comment ""}])]
     (is (= {:event/type :application.event/closed
             :event/time test-time
             :event/actor handler-user-id
-            :application/id 123
+            :application/id app-id
             :application/comment "outdated"}
            (ok-command application
                        {:type :application.command/close
@@ -490,7 +599,7 @@
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}])
+                                    :application/id app-id}])
         injections {:valid-user? #{"deity"}}]
     (testing "required :valid-user? injection"
       (is (= {:errors [{:type :missing-injection :injection :valid-user?}]}
@@ -529,7 +638,7 @@
         (is (= {:event/type :application.event/decision-requested
                 :event/time test-time
                 :event/actor handler-user-id
-                :application/id 123
+                :application/id app-id
                 :application/request-id request-id
                 :application/deciders ["deity"]
                 :application/comment ""}
@@ -553,7 +662,7 @@
           (is (= {:event/type :application.event/decided
                   :event/time test-time
                   :event/actor "deity"
-                  :application/id 123
+                  :application/id app-id
                   :application/request-id request-id
                   :application/decision :approved
                   :application/comment ""}
@@ -577,7 +686,7 @@
           (is (= {:event/type :application.event/decided
                   :event/time test-time
                   :event/actor "deity"
-                  :application/id 123
+                  :application/id app-id
                   :application/request-id request-id
                   :application/decision :rejected
                   :application/comment ""}
@@ -605,18 +714,18 @@
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}
+                                    :application/id app-id}
                                    {:event/type :application.event/member-added
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123
+                                    :application/id app-id
                                     :application/member {:userid "somebody"}}])
         injections {:valid-user? #{"member1" "member2" "somebody" applicant-user-id}}]
     (testing "handler can add members"
       (is (= {:event/type :application.event/member-added
               :event/time test-time
               :event/actor handler-user-id
-              :application/id 123
+              :application/id app-id
               :application/member {:userid "member1"}}
              (ok-command application
                          {:type :application.command/add-member
@@ -658,7 +767,7 @@
       (is (= {:event/type :application.event/member-invited
               :event/time test-time
               :event/actor applicant-user-id
-              :application/id 123
+              :application/id app-id
               :application/member {:name "Member Applicant 1"
                                    :email "member1@applicants.com"}
               :invitation/token "very-secure"}
@@ -672,11 +781,11 @@
       (let [application (apply-events application [{:event/type :application.event/submitted
                                                     :event/time test-time
                                                     :event/actor applicant-user-id
-                                                    :application/id 123}])]
+                                                    :application/id app-id}])]
         (is (= {:event/type :application.event/member-invited
                 :event/time test-time
                 :event/actor handler-user-id
-                :application/id 123
+                :application/id app-id
                 :application/member {:name "Member Applicant 1"
                                      :email "member1@applicants.com"}
                 :invitation/token "very-secure"}
@@ -698,7 +807,7 @@
                                   [{:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}])]
+                                    :application/id app-id}])]
       (testing "applicant can't invite members to submitted application"
         (is (= {:errors [{:type :forbidden}]}
                (fail-command submitted
@@ -721,7 +830,7 @@
                                    {:event/type :application.event/member-invited
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123
+                                    :application/id app-id
                                     :application/member {:name "Some Body" :email "somebody@applicants.com"}
                                     :invitation/token "very-secure"}])
         injections {:valid-user? #{"somebody" "somebody2" applicant-user-id}}]
@@ -730,7 +839,7 @@
       (is (= {:event/type :application.event/member-joined
               :event/time test-time
               :event/actor "somebody"
-              :application/id 123
+              :application/id app-id
               :invitation/token "very-secure"}
              (ok-command application
                          {:type :application.command/accept-invitation
@@ -743,7 +852,7 @@
                                       [{:event/type :application.event/member-added
                                         :event/time test-time
                                         :event/actor applicant-user-id
-                                        :application/id 123
+                                        :application/id app-id
                                         :application/member {:userid "somebody"}}])]
         (is (= {:errors [{:type :already-member :application-id (:id application)}]}
                (fail-command application
@@ -765,7 +874,7 @@
                                       [{:event/type :application.event/member-joined
                                         :event/time test-time
                                         :event/actor "somebody"
-                                        :application/id 123
+                                        :application/id app-id
                                         :invitation/token "very-secure"}])]
         (is (= {:errors [{:type :t.actions.errors/invalid-token :token "very-secure"}]}
                (fail-command application
@@ -778,12 +887,12 @@
                                   [{:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}])]
+                                    :application/id app-id}])]
       (testing "invited member can join submitted application"
         (is (= {:event/type :application.event/member-joined
                 :event/actor "somebody"
                 :event/time test-time
-                :application/id 123
+                :application/id app-id
                 :invitation/token "very-secure"}
                (ok-command application
                            {:type :application.command/accept-invitation
@@ -795,7 +904,7 @@
                                  [{:event/type :application.event/closed
                                    :event/time test-time
                                    :event/actor applicant-user-id
-                                   :application/id 123
+                                   :application/id app-id
                                    :application/comment ""}])]
         (testing "invited member can't join a closed application"
           (is (= {:errors [{:type :forbidden}]}
@@ -811,18 +920,18 @@
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}
+                                    :application/id app-id}
                                    {:event/type :application.event/member-added
                                     :event/time test-time
                                     :event/actor handler-user-id
-                                    :application/id 123
+                                    :application/id app-id
                                     :application/member {:userid "somebody"}}])
         injections {:valid-user? #{"somebody" applicant-user-id handler-user-id}}]
     (testing "applicant can remove members"
       (is (= {:event/type :application.event/member-removed
               :event/time test-time
               :event/actor applicant-user-id
-              :application/id 123
+              :application/id app-id
               :application/member {:userid "somebody"}
               :application/comment "some comment"}
              (ok-command application
@@ -835,7 +944,7 @@
       (is (= {:event/type :application.event/member-removed
               :event/time test-time
               :event/actor handler-user-id
-              :application/id 123
+              :application/id app-id
               :application/member {:userid "somebody"}
               :application/comment ""}
              (ok-command application
@@ -884,19 +993,19 @@
                                    {:event/type :application.event/member-invited
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123
+                                    :application/id app-id
                                     :application/member {:name "Some Body" :email "some@body.com"}
                                     :invitation/token "123456"}
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}])
+                                    :application/id app-id}])
         injections {}]
     (testing "uninvite member by applicant"
       (is (= {:event/type :application.event/member-uninvited
               :event/time test-time
               :event/actor applicant-user-id
-              :application/id 123
+              :application/id app-id
               :application/member {:name "Some Body" :email "some@body.com"}
               :application/comment ""}
              (ok-command application
@@ -909,7 +1018,7 @@
       (is (= {:event/type :application.event/member-uninvited
               :event/time test-time
               :event/actor handler-user-id
-              :application/id 123
+              :application/id app-id
               :application/member {:name "Some Body" :email "some@body.com"}
               :application/comment ""}
              (ok-command application
@@ -933,7 +1042,7 @@
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}])
+                                    :application/id app-id}])
         injections {:valid-user? #{"commenter" "commenter2" "commenter3"}}]
     (testing "required :valid-user? injection"
       (is (= {:errors [{:type :missing-injection :injection :valid-user?}]}
@@ -992,7 +1101,7 @@
                 :application/comment ""
                 :event/time test-time
                 :event/actor handler-user-id
-                :application/id 123}
+                :application/id app-id}
                event-1))
         (is (instance? UUID request-id-2))
         (is (= {:event/type :application.event/comment-requested
@@ -1001,7 +1110,7 @@
                 :application/comment ""
                 :event/time test-time
                 :event/actor handler-user-id
-                :application/id 123}
+                :application/id app-id}
                event-2)))
       (testing "only the requested commenter can comment"
         (is (= {:errors [{:type :forbidden}]}
@@ -1035,7 +1144,7 @@
           (is (= {:event/type :application.event/commented
                   :event/time test-time
                   :event/actor "commenter"
-                  :application/id 123
+                  :application/id app-id
                   :application/request-id request-id-2
                   :application/comment "..."}
                  event)))
@@ -1050,7 +1159,7 @@
           (is (= {:event/type :application.event/commented
                   :event/time test-time
                   :event/actor "commenter2"
-                  :application/id 123
+                  :application/id app-id
                   :application/request-id request-id-1
                   :application/comment "..."}
                  (ok-command application
@@ -1065,7 +1174,7 @@
                                    {:event/type :application.event/submitted
                                     :event/time test-time
                                     :event/actor applicant-user-id
-                                    :application/id 123}])
+                                    :application/id app-id}])
         injections {:valid-user? #{"commenter"}}]
     (testing "handler can remark"
       (let [event (ok-command application
@@ -1078,7 +1187,7 @@
         (is (= {:event/type :application.event/remarked
                 :event/time test-time
                 :event/actor handler-user-id
-                :application/id 123
+                :application/id app-id
                 :application/comment "handler's remark"
                 :application/public false}
                event))))
@@ -1116,7 +1225,7 @@
         (is (= {:event/type :application.event/remarked
                 :event/time test-time
                 :event/actor "commenter"
-                :application/id 123
+                :application/id app-id
                 :application/comment "first remark"
                 :application/public false}
                event-2))
@@ -1137,7 +1246,7 @@
             (is (= {:event/type :application.event/remarked
                     :event/time test-time
                     :event/actor "commenter"
-                    :application/id 123
+                    :application/id app-id
                     :application/comment "second remark"
                     :application/public false}
                    event-2))))))))
@@ -1158,16 +1267,20 @@
                        :form/id 40
                        :workflow/id 50
                        :workflow/type :workflow/dynamic}
-        injections {:application-created-event (fn [{:keys [catalogue-item-ids time actor]}]
-                                                 (is (= applicant-user-id actor)
-                                                     "applicant for new application")
-                                                 (is (= [10 20] catalogue-item-ids)
-                                                     "catalogue items for new application")
-                                                 (assoc created-event
-                                                        :event/time time
-                                                        :event/actor actor
-                                                        :application/id new-app-id
-                                                        :application/external-id "2019/66"))}
+        injections {:get-catalogue-item {10 {:id 10
+                                             :resid "urn:11"
+                                             :formid 40
+                                             :wfid 50}
+                                         20 {:id 20
+                                             :resid "urn:21"
+                                             :formid 40
+                                             :wfid 50}}
+                    :get-catalogue-item-licenses {10 []
+                                                  20 []}
+                    :get-workflow {50 {:workflow {:type :workflow/dynamic}}}
+                    :allocate-application-ids! (fn [_time]
+                                                 {:application/id new-app-id
+                                                  :application/external-id "2019/66"})}
         application (apply-events nil [created-event
                                        {:event/type :application.event/draft-saved
                                         :event/time test-time
diff --git a/test/clj/rems/db/test_applications.clj b/test/clj/rems/db/test_applications.clj
index 13ec2bbed..3298ea2d5 100644
--- a/test/clj/rems/db/test_applications.clj
+++ b/test/clj/rems/db/test_applications.clj
@@ -3,15 +3,16 @@
             [clojure.test :refer :all]
             [clojure.test.check.generators :as generators]
             [rems.application.events :as events]
-            [rems.db.applications :refer [application-created-event application-external-id!]]
+            [rems.db.applications :as applications]
             [rems.db.core :as db]
             [rems.db.events :as db-events]
             [rems.db.test-data :as test-data]
             [rems.db.testing :refer [test-db-fixture rollback-db-fixture test-data-fixture]]
             [rems.util :refer [try-catch-ex]]
             [schema-generators.generators :as sg])
-  (:import (org.joda.time DateTime DateTimeZone)
-           (clojure.lang ExceptionInfo)))
+  (:import [clojure.lang ExceptionInfo]
+           [java.util UUID]
+           [org.joda.time DateTime DateTimeZone]))
 
 (use-fixtures
   :once
@@ -42,144 +43,37 @@
           json (db-events/event->json event)]
       (is (str/includes? json "\"event/time\":\"2020-01-01T10:00:00.000Z\"")))))
 
-(deftest test-application-created-event
-  (let [wf-id (test-data/create-dynamic-workflow! {})
-        form-id (test-data/create-form! {})
-        res-id (test-data/create-resource! {:resource-ext-id "res1"})
-        cat-id (test-data/create-catalogue-item! {:resource-id res-id
-                                                  :form-id form-id
-                                                  :workflow-id wf-id})]
-
-    (testing "minimal application"
-      (is (= {:event/type :application.event/created
-              :event/actor "alice"
-              :event/time (DateTime. 1000)
-              :application/id 42
-              :application/external-id "1970/1"
-              :application/resources [{:catalogue-item/id cat-id
-                                       :resource/ext-id "res1"}]
-              :application/licenses []
-              :form/id form-id
-              :workflow/id wf-id
-              :workflow/type :workflow/dynamic}
-             (application-created-event {:application-id 42
-                                         :catalogue-item-ids [cat-id]
-                                         :time (DateTime. 1000)
-                                         :actor "alice"}))))
-
-    (testing "multiple resources"
-      (let [res-id2 (test-data/create-resource! {:resource-ext-id "res2"})
-            cat-id2 (test-data/create-catalogue-item! {:resource-id res-id2
-                                                       :form-id form-id
-                                                       :workflow-id wf-id})]
-        (is (= {:event/type :application.event/created
-                :event/actor "alice"
-                :event/time (DateTime. 1000)
-                :application/id 42
-                :application/external-id "1970/2"
-                :application/resources [{:catalogue-item/id cat-id
-                                         :resource/ext-id "res1"}
-                                        {:catalogue-item/id cat-id2
-                                         :resource/ext-id "res2"}]
-                :application/licenses []
-                :form/id form-id
-                :workflow/id wf-id
-                :workflow/type :workflow/dynamic}
-               (application-created-event {:application-id 42
-                                           :catalogue-item-ids [cat-id cat-id2]
-                                           :time (DateTime. 1000)
-                                           :actor "alice"})))))
-
-    (testing "error: zero catalogue items"
-      (is (thrown-with-msg? AssertionError #"catalogue item not specified"
-                            (application-created-event {:application-id 42
-                                                        :catalogue-item-ids []
-                                                        :time (DateTime. 1000)
-                                                        :actor "alice"}))))
-
-    (testing "error: non-existing catalogue items"
-      (is (thrown-with-msg? AssertionError #"catalogue item not found"
-                            (application-created-event {:application-id 42
-                                                        :catalogue-item-ids [999999]
-                                                        :time (DateTime. 1000)
-                                                        :actor "alice"}))))
-
-    (testing "error: catalogue items with different forms"
-      (let [form-id2 (test-data/create-form! {})
-            res-id2 (test-data/create-resource! {:resource-ext-id "res2+"})
-            cat-id2 (test-data/create-catalogue-item! {:resource-id res-id2
-                                                       :form-id form-id2
-                                                       :workflow-id wf-id})]
-        (is (thrown-with-msg? AssertionError #"catalogue items did not have the same form"
-                              (application-created-event {:application-id 42
-                                                          :catalogue-item-ids [cat-id cat-id2]
-                                                          :time (DateTime. 1000)
-                                                          :actor "alice"})))))
-
-    (testing "error: catalogue items with different workflows"
-      (let [wf-id2 (test-data/create-dynamic-workflow! {})
-            res-id2 (test-data/create-resource! {:resource-ext-id "res2++"})
-            cat-id2 (test-data/create-catalogue-item! {:resource-id res-id2
-                                                       :form-id form-id
-                                                       :workflow-id wf-id2})]
-        (is (thrown-with-msg? AssertionError #"catalogue items did not have the same workflow"
-                              (application-created-event {:application-id 42
-                                                          :catalogue-item-ids [cat-id cat-id2]
-                                                          :time (DateTime. 1000)
-                                                          :actor "alice"})))))
-
+(deftest test-get-catalogue-item-licenses
+  (let [form-id (test-data/create-form! {})]
     (testing "resource licenses"
       (let [lic-id (test-data/create-license! {})
-            res-id2 (test-data/create-resource! {:resource-ext-id "res2+++"
-                                                 :license-ids [lic-id]})
-            cat-id2 (test-data/create-catalogue-item! {:resource-id res-id2
-                                                       :form-id form-id
-                                                       :workflow-id wf-id})]
-        (is (= {:event/type :application.event/created
-                :event/actor "alice"
-                :event/time (DateTime. 1000)
-                :application/id 42
-                :application/external-id "1970/3"
-                :application/resources [{:catalogue-item/id cat-id2
-                                         :resource/ext-id "res2+++"}]
-                :application/licenses [{:license/id lic-id}]
-                :form/id form-id
-                :workflow/id wf-id
-                :workflow/type :workflow/dynamic}
-               (application-created-event {:application-id 42
-                                           :catalogue-item-ids [cat-id2]
-                                           :time (DateTime. 1000)
-                                           :actor "alice"})))))
+            wf-id (test-data/create-dynamic-workflow! {})
+            res-id (test-data/create-resource! {:resource-ext-id (str (UUID/randomUUID))
+                                                :license-ids [lic-id]})
+            cat-id (test-data/create-catalogue-item! {:resource-id res-id
+                                                      :form-id form-id
+                                                      :workflow-id wf-id})]
+        (is (= [lic-id]
+               (map :id (applications/get-catalogue-item-licenses cat-id))))))
 
     (testing "workflow licenses"
       (let [lic-id (test-data/create-license! {})
-            wf-id2 (test-data/create-dynamic-workflow! {})
-            _ (db/create-workflow-license! {:wfid wf-id2 :licid lic-id})
-            cat-id2 (test-data/create-catalogue-item! {:resource-id res-id
-                                                       :form-id form-id
-                                                       :workflow-id wf-id2})]
-        (is (= {:event/type :application.event/created
-                :event/actor "alice"
-                :event/time (DateTime. 1000)
-                :application/id 42
-                :application/external-id "1970/4"
-                :application/resources [{:catalogue-item/id cat-id2
-                                         :resource/ext-id "res1"}]
-                :application/licenses [{:license/id lic-id}]
-                :form/id form-id
-                :workflow/id wf-id2
-                :workflow/type :workflow/dynamic}
-               (application-created-event {:application-id 42
-                                           :catalogue-item-ids [cat-id2]
-                                           :time (DateTime. 1000)
-                                           :actor "alice"})))))))
+            wf-id (test-data/create-dynamic-workflow! {})
+            _ (db/create-workflow-license! {:wfid wf-id :licid lic-id})
+            res-id (test-data/create-resource! {:resource-ext-id (str (UUID/randomUUID))})
+            cat-id (test-data/create-catalogue-item! {:resource-id res-id
+                                                      :form-id form-id
+                                                      :workflow-id wf-id})]
+        (is (= [lic-id]
+               (map :id (applications/get-catalogue-item-licenses cat-id))))))))
 
 (deftest test-application-external-id!
-  (is (= [] (db/get-external-ids {:prefix "1981"})))
-  (is (= [] (db/get-external-ids {:prefix "1980"})))
-  (is (= "1981/1" (application-external-id! (DateTime. #inst "1981-03-02"))))
-  (is (= "1981/2" (application-external-id! (DateTime. #inst "1981-01-01"))))
-  (is (= "1981/3" (application-external-id! (DateTime. #inst "1981-04-03"))))
-  (is (= "1980/1" (application-external-id! (DateTime. #inst "1980-12-12"))))
-  (is (= "1980/2" (application-external-id! (DateTime. #inst "1980-12-12"))))
-  (is (= "1981/4" (application-external-id! (DateTime. #inst "1981-04-01")))))
+  (let [application-external-id! @#'applications/application-external-id!]
+    (is (= [] (db/get-external-ids {:prefix "1981"})))
+    (is (= [] (db/get-external-ids {:prefix "1980"})))
+    (is (= "1981/1" (application-external-id! (DateTime. #inst "1981-03-02"))))
+    (is (= "1981/2" (application-external-id! (DateTime. #inst "1981-01-01"))))
+    (is (= "1981/3" (application-external-id! (DateTime. #inst "1981-04-03"))))
+    (is (= "1980/1" (application-external-id! (DateTime. #inst "1980-12-12"))))
+    (is (= "1980/2" (application-external-id! (DateTime. #inst "1980-12-12"))))
+    (is (= "1981/4" (application-external-id! (DateTime. #inst "1981-04-01"))))))
diff --git a/test/clj/rems/db/test_licenses.clj b/test/clj/rems/db/test_licenses.clj
index e25e977e3..08003b7c7 100644
--- a/test/clj/rems/db/test_licenses.clj
+++ b/test/clj/rems/db/test_licenses.clj
@@ -23,24 +23,16 @@
             [{:id 1 :type "text" :title "default title 1" :textcontent "default content 1"}
              {:id 2 :type "link" :title "default title 2" :textcontent "default content 2"}])))))
 
-(deftest test-get-all-licenses
+(deftest test-get-active-licenses
   (let [today (time/now)
         yesterday (time/minus today (time/days 1))
         expired-license-end (time/plus yesterday (time/hours 1))
         just-created-license-start (time/minus today (time/hours 1))]
     (with-redefs [db/get-license-localizations (constantly [])
-                  db/get-all-licenses (fn [] [{:id :always :start nil :end nil}
-                                              {:id :always2 :start nil :end nil}
-                                              {:id :expired :start nil :end expired-license-end}
-                                              {:id :just-created :start just-created-license-start :end nil}])]
-      (testing "expired field is added"
-        (is (= [{:id :always :expired false}
-                {:id :always2 :expired false}
-                {:id :expired :expired true}
-                {:id :just-created :expired false}]
-               (map #(select-keys % [:id :expired]) (get-all-licenses {})))))
-      (testing "filters"
-        (is (= [:expired]
-               (map :id (get-all-licenses {:expired true}))))
-        (is (= [:always :always2 :just-created]
-               (map :id (get-all-licenses {:expired false}))))))))
+                  db/get-licenses (fn [params] [{:id :always :start nil :end nil}
+                                                {:id :always :start nil :end nil}
+                                                {:id :expired :start nil :end expired-license-end}
+                                                {:id :just-created :start just-created-license-start :end nil}])]
+      (is (= 1 (count (:always (group-by :id (get-active-licenses today nil))))) "should be deduplicated")
+      (is (= #{:always :just-created} (set (map :id (get-active-licenses today nil)))))
+      (is (= #{:always :expired} (set (map :id (get-active-licenses yesterday nil))))))))
diff --git a/test/clj/rems/db/test_transactions.clj b/test/clj/rems/db/test_transactions.clj
index 89f2f93b2..2ae9b3276 100644
--- a/test/clj/rems/db/test_transactions.clj
+++ b/test/clj/rems/db/test_transactions.clj
@@ -1,6 +1,5 @@
 (ns ^:integration rems.db.test-transactions
-  (:require [clj-time.core :as time]
-            [clojure.test :refer :all]
+  (:require [clojure.test :refer :all]
             [conman.core :as conman]
             [rems.application.commands :as commands]
             [rems.db.core :as db]
@@ -117,8 +116,11 @@
         (prn 'all-events-reader-results (count all-events-reader-results)))
 
       (testing "all commands succeeded"
-        (is (seq writer-results))
-        (is (every? nil? writer-results)) ; successful commands return nil
+        (is (seq writer-results)
+            "at least one result")
+        (is (every? #(= [:events] (keys %))
+                    writer-results)
+            "no errors")
         (is (= (count writer-results)
                (count writer-attempts))
             "should have no transaction conflicts"))