clauding
This commit is contained in:
5
.claude/settings.json
Normal file
5
.claude/settings.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"enabledPlugins": {
|
||||||
|
"playwright@claude-plugins-official": true
|
||||||
|
}
|
||||||
|
}
|
||||||
247
.claude/skills/testing-conventions/SKILL.md
Normal file
247
.claude/skills/testing-conventions/SKILL.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
---
|
||||||
|
name: testing-conventions
|
||||||
|
description: Describe the way that tests should be authored, conventions, tools, helpers, superceding any conventions found in existing tests.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Testing Conventions Skill
|
||||||
|
|
||||||
|
This skill documents the testing conventions for `test/clj/auto_ap/`.
|
||||||
|
|
||||||
|
## Test Focus: User-Observable Behavior
|
||||||
|
|
||||||
|
**Primary rule**: Test user-observable behavior. If an endpoint or function makes a database change, verify the change by querying the database directly rather than asserting on markup.
|
||||||
|
|
||||||
|
**other rules**:
|
||||||
|
1. Don't test the means of doing work. For example, if there is a middleware that makes something available on a request, don't bother testing that wrapper.
|
||||||
|
2. prefer :refer testing imports, rather than :as reference
|
||||||
|
3. Prefer structured edits from clojure-mcp
|
||||||
|
|
||||||
|
### When to Assert on Database State
|
||||||
|
|
||||||
|
When testing an endpoint that modifies data:
|
||||||
|
1. Verify the database change by querying the entity directly
|
||||||
|
2. Use `dc/pull` or `dc/q` to verify the data was stored correctly
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
;; CORRECT: Verify the database change directly
|
||||||
|
(deftest test-create-transaction
|
||||||
|
(let [result @(post-create-transaction {:amount 100.0})]
|
||||||
|
(let [entity (dc/pull (dc/db conn) [:db/id :transaction/amount] (:transaction/id result))]
|
||||||
|
(is (= 100.0 (:transaction/amount entity))))))
|
||||||
|
|
||||||
|
;; CORRECT: Verify response status and headers
|
||||||
|
(is (= 201 (:status response)))
|
||||||
|
(is (= "application/json" (get-in response [:headers "content-type"])))
|
||||||
|
|
||||||
|
;; CORRECT: Check for expected text content
|
||||||
|
(is (re-find #"Transaction created" (get-in response [:body "message"])))
|
||||||
|
```
|
||||||
|
|
||||||
|
### When Markup Testing is Acceptable
|
||||||
|
|
||||||
|
Markup testing (HTML/SSR response bodies) is acceptable when:
|
||||||
|
- Validating response status codes and headers
|
||||||
|
- Checking for presence/absence of specific text strings
|
||||||
|
- Verifying small, expected elements within the markup
|
||||||
|
- Testing SSR component rendering
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
;; ACCEPTABLE: Response codes and headers
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (= "application/json" (get-in response [:headers "content-type"])))
|
||||||
|
|
||||||
|
;; ACCEPTABLE: Text content within markup
|
||||||
|
(is (re-find #"Transaction found" response-body))
|
||||||
|
|
||||||
|
;; ACCEPTABLE: Small element checks
|
||||||
|
(is (re-find #">Amount: \$100\.00<" response-body))
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Avoid Markup Testing
|
||||||
|
|
||||||
|
Do not use markup assertions for:
|
||||||
|
- Verifying complex data structures (use database queries instead)
|
||||||
|
- Complex nested content that's easier to query
|
||||||
|
- Business logic verification (test behavior, not presentation)
|
||||||
|
|
||||||
|
## Database Setup
|
||||||
|
|
||||||
|
All tests in `test/clj/auto_ap/` use a shared database fixture (`wrap-setup`) that:
|
||||||
|
1. Creates a temporary in-memory Datomic database (`datomic:mem://test`)
|
||||||
|
2. Loads the full schema from `io/resources/schema.edn`
|
||||||
|
3. Installs custom Datomic functions from `io/resources/functions.edn`
|
||||||
|
4. Cleans up the database after each test
|
||||||
|
|
||||||
|
## Using the Fixture
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(ns my-test
|
||||||
|
(:require
|
||||||
|
[auto-ap.integration.util :refer [wrap-setup]]
|
||||||
|
[clojure.test :as t]))
|
||||||
|
|
||||||
|
(use-fixtures :each wrap-setup)
|
||||||
|
|
||||||
|
(deftest my-test
|
||||||
|
;; tests here can access the test database
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helper Functions
|
||||||
|
|
||||||
|
`test/clj/auto_ap/integration/util.clj` provides helper functions for creating test data:
|
||||||
|
|
||||||
|
### Identity Helpers
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
;; Add a unique string to avoid collisions
|
||||||
|
(str "CLIENT" (rand-int 100000))
|
||||||
|
(str "INVOICE " (rand-int 1000000))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Entity Builders
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
;; Client
|
||||||
|
(test-client
|
||||||
|
[:db/id "client-id"
|
||||||
|
:client/code "CLIENT123"
|
||||||
|
:client/locations ["DT" "MH"]
|
||||||
|
:client/bank-accounts [:bank-account-id]])
|
||||||
|
|
||||||
|
;; Vendor
|
||||||
|
(test-vendor
|
||||||
|
[:db/id "vendor-id"
|
||||||
|
:vendor/name "Vendorson"
|
||||||
|
:vendor/default-account "test-account-id"])
|
||||||
|
|
||||||
|
;; Bank Account
|
||||||
|
(test-bank-account
|
||||||
|
[:db/id "bank-account-id"
|
||||||
|
:bank-account/code "TEST-BANK-123"
|
||||||
|
:bank-account/type :bank-account-type/check])
|
||||||
|
|
||||||
|
;; Transaction
|
||||||
|
(test-transaction
|
||||||
|
[:db/id "transaction-id"
|
||||||
|
:transaction/date #inst "2022-01-01"
|
||||||
|
:transaction/client "test-client-id"
|
||||||
|
:transaction/bank-account "test-bank-account-id"
|
||||||
|
:transaction/id (str (java.util.UUID/randomUUID))
|
||||||
|
:transaction/amount 100.0
|
||||||
|
:transaction/description-original "original description"])
|
||||||
|
|
||||||
|
;; Payment
|
||||||
|
(test-payment
|
||||||
|
[:db/id "test-payment-id"
|
||||||
|
:payment/date #inst "2022-01-01"
|
||||||
|
:payment/client "test-client-id"
|
||||||
|
:payment/bank-account "test-bank-account-id"
|
||||||
|
:payment/type :payment-type/check
|
||||||
|
:payment/vendor "test-vendor-id"
|
||||||
|
:payment/amount 100.0])
|
||||||
|
|
||||||
|
;; Invoice
|
||||||
|
(test-invoice
|
||||||
|
[:db/id "test-invoice-id"
|
||||||
|
:invoice/date #inst "2022-01-01"
|
||||||
|
:invoice/client "test-client-id"
|
||||||
|
:invoice/status :invoice-status/unpaid
|
||||||
|
:invoice/import-status :import-status/imported
|
||||||
|
:invoice/total 100.0
|
||||||
|
:invoice/outstanding-balance 100.00
|
||||||
|
:invoice/vendor "test-vendor-id"
|
||||||
|
:invoice/invoice-number "INVOICE 123456"
|
||||||
|
:invoice/expense-accounts
|
||||||
|
[{:invoice-expense-account/account "test-account-id"
|
||||||
|
:invoice-expense-account/amount 100.0
|
||||||
|
:invoice-expense-account/location "DT"}]])
|
||||||
|
|
||||||
|
;; Account
|
||||||
|
(test-account
|
||||||
|
[:db/id "account-id"
|
||||||
|
:account/name "Account"
|
||||||
|
:account/type :account-type/asset])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Data Setup (`setup-test-data`)
|
||||||
|
|
||||||
|
Creates a minimal but complete dataset for testing:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(defn setup-test-data [data]
|
||||||
|
(:tempids @(dc/transact conn (into data
|
||||||
|
[(test-account :db/id "test-account-id")
|
||||||
|
(test-client :db/id "test-client-id"
|
||||||
|
:client/bank-accounts [(test-bank-account :db/id "test-bank-account-id")])
|
||||||
|
(test-vendor :db/id "test-vendor-id")
|
||||||
|
{:db/id "accounts-payable-id"
|
||||||
|
:account/name "Accounts Payable"
|
||||||
|
:db/ident :account/accounts-payable
|
||||||
|
:account/numeric-code 21000
|
||||||
|
:account/account-set "default"}]))))
|
||||||
|
```
|
||||||
|
|
||||||
|
Use like:
|
||||||
|
```clojure
|
||||||
|
(let [{:strs [test-client-id test-bank-account-id test-vendor-id]} (setup-test-data [])]
|
||||||
|
...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Token Helpers
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
;; Admin token
|
||||||
|
(admin-token)
|
||||||
|
|
||||||
|
;; User token (optionally scoped to specific client)
|
||||||
|
(user-token) ; Default: client-id 1
|
||||||
|
(user-token client-id) ; Scoped to specific client
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(ns my-test
|
||||||
|
(:require
|
||||||
|
[clojure.test :as t]
|
||||||
|
[auto-ap.datomic :refer [conn]]
|
||||||
|
[auto-ap.integration.util :refer [wrap-setup admin-token setup-test-data test-transaction]]))
|
||||||
|
|
||||||
|
(use-fixtures :each wrap-setup)
|
||||||
|
|
||||||
|
(deftest test-transaction-import
|
||||||
|
(testing "Should import a transaction"
|
||||||
|
(let [{:strs [client-id bank-account-id]} (setup-test-data [])
|
||||||
|
tx-result @(dc/transact conn
|
||||||
|
[(test-transaction
|
||||||
|
{:db/id "test-tx-id"
|
||||||
|
:transaction/client client-id
|
||||||
|
:transaction/bank-account bank-account-id
|
||||||
|
:transaction/amount 50.0})])]
|
||||||
|
(is (= 1 (count (:tx-data tx-result))))
|
||||||
|
;; Verify by querying the database, not markup
|
||||||
|
(let [entity (dc/pull (dc/db conn) [:transaction/amount] (:db/id tx-result))]
|
||||||
|
(is (= 50.0 (:transaction/amount entity)))))))
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note on Temp IDs
|
||||||
|
|
||||||
|
Test data often uses string-based temp IDs like `"client-id"`, `"bank-account-id"`, etc. When transacting, the returned `:tempids` map maps these symbolic IDs to Datomic's internal entity IDs:
|
||||||
|
|
||||||
|
```clojure
|
||||||
|
(let [{:strs [client-id bank-account-id]} (:tempids @(dc/transact conn txes))]
|
||||||
|
...)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memory Database
|
||||||
|
|
||||||
|
All tests use `datomic:mem://test` - an in-memory database. This ensures:
|
||||||
|
- Tests are fast
|
||||||
|
- Tests don't interfere with each other
|
||||||
|
- No setup required to run tests locally
|
||||||
|
|
||||||
|
The database is automatically deleted after each test completes.
|
||||||
|
|
||||||
|
# running tests
|
||||||
|
use leiningen to run tests
|
||||||
@@ -110,6 +110,7 @@
|
|||||||
[commons-codec "1.12"]]
|
[commons-codec "1.12"]]
|
||||||
:plugins [[lein-ring "0.9.7"]
|
:plugins [[lein-ring "0.9.7"]
|
||||||
#_[lein-cljsbuild "1.1.5"]
|
#_[lein-cljsbuild "1.1.5"]
|
||||||
|
[dev.weavejester/lein-cljfmt "0.15.6"]
|
||||||
[lein-ancient "0.6.15"]]
|
[lein-ancient "0.6.15"]]
|
||||||
:clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
|
:clean-targets ^{:protect false} ["resources/public/js/compiled" "target"]
|
||||||
#_#_:ring {:handler auto-ap.handler/app}
|
#_#_:ring {:handler auto-ap.handler/app}
|
||||||
|
|||||||
221
test/clj/auto_ap/ssr/admin/accounts_test.clj
Normal file
221
test/clj/auto_ap/ssr/admin/accounts_test.clj
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
(ns auto-ap.ssr.admin.accounts-test
|
||||||
|
(:require
|
||||||
|
[datomic.api :as dc :refer [q]]
|
||||||
|
[auto-ap.datomic :refer [conn]]
|
||||||
|
[auto-ap.integration.util :refer [wrap-setup test-account
|
||||||
|
admin-token user-token
|
||||||
|
setup-test-data test-client]]
|
||||||
|
[auto-ap.ssr.admin.accounts :as sut]
|
||||||
|
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||||
|
[malli.core :as mc]))
|
||||||
|
|
||||||
|
(use-fixtures :each wrap-setup)
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Schemas
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest query-schema-test
|
||||||
|
(testing "Should validate correct query params"
|
||||||
|
(is (mc/validate sut/query-schema
|
||||||
|
{:date-range {:start-date #inst "2021-01-01" :end-date #inst "2021-12-31"}
|
||||||
|
:type "asset"
|
||||||
|
:name "Cash"
|
||||||
|
:code 11101
|
||||||
|
:page 1
|
||||||
|
:per-page 50
|
||||||
|
:sort "name"
|
||||||
|
:sort-dir "asc"})))
|
||||||
|
(testing "Should allow optional fields"
|
||||||
|
(is (mc/validate sut/query-schema
|
||||||
|
{:page 1
|
||||||
|
:per-page 50})))
|
||||||
|
(testing "Should reject invalid type"
|
||||||
|
(is (not (mc/validate sut/query-schema
|
||||||
|
{:type "invalid-type"})))))
|
||||||
|
|
||||||
|
(deftest form-schema-test
|
||||||
|
(testing "Should validate valid account form"
|
||||||
|
(is (mc/validate sut/form-schema
|
||||||
|
{:db/id "new"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Test Account"
|
||||||
|
:account/type :account-type/asset
|
||||||
|
:account/location "DT"
|
||||||
|
:account/invoice-allowance :allowance/allowed
|
||||||
|
:account/vendor-allowance :allowance/allowed
|
||||||
|
:account/applicability :account-applicability/default})))
|
||||||
|
(testing "Should allow updating existing account"
|
||||||
|
(is (mc/validate sut/form-schema
|
||||||
|
{:db/id 123
|
||||||
|
:account/numeric-code 11102
|
||||||
|
:account/name "Updated Account"
|
||||||
|
:account/type :account-type/liability})))
|
||||||
|
(testing "Should require name"
|
||||||
|
(is (not (mc/validate sut/form-schema
|
||||||
|
{:account/numeric-code 11101}))))
|
||||||
|
(testing "Should reject invalid type"
|
||||||
|
(is (not (mc/validate sut/form-schema
|
||||||
|
{:account/numeric-code 11101
|
||||||
|
:account/name "Test"
|
||||||
|
:account/type :invalid-type}))))
|
||||||
|
(testing "Should accept client overrides"
|
||||||
|
(is (mc/validate sut/form-schema
|
||||||
|
{:account/numeric-code 11101
|
||||||
|
:account/name "Test"
|
||||||
|
:account/client-overrides
|
||||||
|
[{:account-client-override/client 1
|
||||||
|
:account-client-override/name "Override"}]}))))
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Fetch IDs
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest fetch-ids-by-type-test
|
||||||
|
(testing "Should filter by account type"
|
||||||
|
(let [_ (setup-test-data [(test-account :db/id "acc-1"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Asset Account"
|
||||||
|
:account/type :account-type/asset)
|
||||||
|
(test-account :db/id "acc-2"
|
||||||
|
:account/numeric-code 11102
|
||||||
|
:account/name "Liability Account"
|
||||||
|
:account/type :account-type/liability)])]
|
||||||
|
(let [results (dc/q '[:find ?e ?n ?nc
|
||||||
|
:in $
|
||||||
|
:where [?e :account/name ?n]
|
||||||
|
[?e :account/numeric-code ?nc]]
|
||||||
|
(dc/db conn))]
|
||||||
|
(is (= 2 (count results)))))))
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Fetch Page
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest fetch-page-test
|
||||||
|
(testing "Should return empty results when no accounts exist"
|
||||||
|
(let [db (dc/db conn)
|
||||||
|
[_ count] (sut/fetch-page {:query-params {}})]
|
||||||
|
(is (= 0 count))
|
||||||
|
(is (empty? _)))))
|
||||||
|
|
||||||
|
(deftest fetch-page-with-name-filter-test
|
||||||
|
(testing "Should filter by name (case-insensitive)"
|
||||||
|
(let [_ (setup-test-data [(test-account :db/id "acc-1"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Asset Account")
|
||||||
|
(test-account :db/id "acc-2"
|
||||||
|
:account/numeric-code 11102
|
||||||
|
:account/name "Liability Account")])]
|
||||||
|
(let [[results count] (sut/fetch-page
|
||||||
|
{:query-params {:name "asset"}})]
|
||||||
|
(is (= 1 count))
|
||||||
|
(is (= 1 (count results)))
|
||||||
|
(is (= "Asset Account" (:account/name (first results))))))))
|
||||||
|
|
||||||
|
(deftest fetch-page-with-code-filter-test
|
||||||
|
(testing "Should filter by numeric code"
|
||||||
|
(let [_ (setup-test-data [(test-account :db/id "acc-1"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Account 1")
|
||||||
|
(test-account :db/id "acc-2"
|
||||||
|
:account/numeric-code 11102
|
||||||
|
:account/name "Account 2")])]
|
||||||
|
(let [[results count] (sut/fetch-page
|
||||||
|
{:query-params {:code 11102}})]
|
||||||
|
(is (= 1 count))
|
||||||
|
(is (= 1 (count results)))
|
||||||
|
(is (= "Account 2" (:account/name (first results))))))))
|
||||||
|
|
||||||
|
(deftest fetch-page-with-type-filter-test
|
||||||
|
(testing "Should filter by account type"
|
||||||
|
(let [_ (setup-test-data [(test-account :db/id "acc-1"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Asset Account"
|
||||||
|
:account/type :account-type/asset)
|
||||||
|
(test-account :db/id "acc-2"
|
||||||
|
:account/numeric-code 11102
|
||||||
|
:account/name "Liability Account"
|
||||||
|
:account/type :account-type/liability)])]
|
||||||
|
(let [[results count] (sut/fetch-page
|
||||||
|
{:query-params {:type "liability"}})]
|
||||||
|
(is (= 1 count))
|
||||||
|
(is (= 1 (count results)))
|
||||||
|
(is (= "Liability Account" (:account/name (first results))))))))
|
||||||
|
|
||||||
|
(deftest fetch-page-pagination-test
|
||||||
|
(testing "Should apply pagination"
|
||||||
|
(let [_ (setup-test-data
|
||||||
|
(for [i (range 15)]
|
||||||
|
(test-account :db/id (str "acc-" i)
|
||||||
|
:account/numeric-code (+ 11100 i)
|
||||||
|
:account/name (str "Account " i))))]
|
||||||
|
(let [[results count] (sut/fetch-page
|
||||||
|
{:query-params {:page 1 :per-page 10}})]
|
||||||
|
(is (= 10 count))
|
||||||
|
(is (= 10 (count results)))
|
||||||
|
(is (= "Account 0" (:account/name (first results))))
|
||||||
|
(is (= "Account 9" (:account/name (last results))))))))
|
||||||
|
|
||||||
|
(deftest fetch-page-sort-test
|
||||||
|
(testing "Should sort by name ascending"
|
||||||
|
(let [_ (setup-test-data
|
||||||
|
[(test-account :db/id "acc-3"
|
||||||
|
:account/numeric-code 11103
|
||||||
|
:account/name "Charlie")
|
||||||
|
(test-account :db/id "acc-1"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Alpha")
|
||||||
|
(test-account :db/id "acc-2"
|
||||||
|
:account/numeric-code 11102
|
||||||
|
:account/name "Beta")])]
|
||||||
|
(let [[results count] (sut/fetch-page
|
||||||
|
{:query-params {:sort "name" :sort-dir "asc"}})]
|
||||||
|
(is (= 3 count))
|
||||||
|
(is (= "Alpha" (:account/name (first results))))
|
||||||
|
(is (= "Charlie" (:account/name (last results)))))))
|
||||||
|
|
||||||
|
(testing "Should sort by code ascending"
|
||||||
|
(let [_ (setup-test-data
|
||||||
|
[(test-account :db/id "acc-2"
|
||||||
|
:account/numeric-code 11102
|
||||||
|
:account/name "Beta")
|
||||||
|
(test-account :db/id "acc-1"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Alpha")])]
|
||||||
|
(let [[results count] (sut/fetch-page
|
||||||
|
{:query-params {:sort "code" :sort-dir "asc"}})]
|
||||||
|
(is (= 2 count))
|
||||||
|
(is (= 11101 (:account/numeric-code (first results))))
|
||||||
|
(is (= 11102 (:account/numeric-code (last results))))))))
|
||||||
|
|
||||||
|
(deftest fetch-page-combine-filters-test
|
||||||
|
(testing "Should combine name and type filters"
|
||||||
|
(let [_ (setup-test-data
|
||||||
|
[(test-account :db/id "acc-1"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Cash Asset"
|
||||||
|
:account/type :account-type/asset)
|
||||||
|
(test-account :db/id "acc-2"
|
||||||
|
:account/numeric-code 11102
|
||||||
|
:account/name "Cash Liability"
|
||||||
|
:account/type :account-type/liability)
|
||||||
|
(test-account :db/id "acc-3"
|
||||||
|
:account/numeric-code 11103
|
||||||
|
:account/name "Bank Asset"
|
||||||
|
:account/type :account-type/asset)])]
|
||||||
|
(let [[results count] (sut/fetch-page
|
||||||
|
{:query-params {:name "cash" :type "asset"}})]
|
||||||
|
(is (= 1 count))
|
||||||
|
(is (= 1 (count results)))
|
||||||
|
(is (= "Cash Asset" (:account/name (first results))))))))
|
||||||
|
|
||||||
|
(deftest fetch-page-mixed-case-name-filter-test
|
||||||
|
(testing "Should handle case-insensitive name filtering"
|
||||||
|
(let [_ (setup-test-data [(test-account :db/id "acc-1"
|
||||||
|
:account/numeric-code 11101
|
||||||
|
:account/name "Cash Account")])]
|
||||||
|
(let [[results count] (sut/fetch-page
|
||||||
|
{:query-params {:name "CASH"}})]
|
||||||
|
(is (= 1 count))
|
||||||
|
(is (= 1 (count results)))))))
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
(:require [clojure.test :refer [deftest testing is]]
|
(:require [clojure.test :refer [deftest testing is]]
|
||||||
[auto-ap.ssr.invoice.new-invoice-wizard :as sut9]))
|
[auto-ap.ssr.invoice.new-invoice-wizard :as sut9]))
|
||||||
|
|
||||||
|
|
||||||
(deftest maybe-spread-locations-test
|
(deftest maybe-spread-locations-test
|
||||||
(testing "Shared amount correctly spread across multiple locations"
|
(testing "Shared amount correctly spread across multiple locations"
|
||||||
(let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount 100.0
|
(let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount 100.0
|
||||||
@@ -30,8 +29,6 @@
|
|||||||
:invoice-expense-account/location "Location 2"}]
|
:invoice-expense-account/location "Location 2"}]
|
||||||
(map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result))))))
|
(map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result))))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
(testing "Shared amount correctly spread with leftovers"
|
(testing "Shared amount correctly spread with leftovers"
|
||||||
(let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount 100.0
|
(let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount 100.0
|
||||||
:invoice-expense-account/location "Shared"}]
|
:invoice-expense-account/location "Shared"}]
|
||||||
@@ -77,14 +74,14 @@
|
|||||||
{:invoice-expense-account/amount -50.66
|
{:invoice-expense-account/amount -50.66
|
||||||
:invoice-expense-account/location "Location 2"}]
|
:invoice-expense-account/location "Location 2"}]
|
||||||
(map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result))))))
|
(map #(select-keys % #{:invoice-expense-account/amount :invoice-expense-account/location}) (:invoice/expense-accounts result))))))
|
||||||
|
|
||||||
(testing "Leftovers should not exceed a single cent"
|
(testing "Leftovers should not exceed a single cent"
|
||||||
(let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount -100
|
(let [invoice {:invoice/expense-accounts [{:invoice-expense-account/amount -100
|
||||||
:invoice-expense-account/location "Shared"}
|
:invoice-expense-account/location "Shared"}
|
||||||
{:invoice-expense-account/amount -5
|
{:invoice-expense-account/amount -5
|
||||||
:invoice-expense-account/location "Shared"}]
|
:invoice-expense-account/location "Shared"}]
|
||||||
:invoice/total -101}
|
:invoice/total -101}
|
||||||
result (sut9/maybe-spread-locations invoice ["Location 1" ])]
|
result (sut9/maybe-spread-locations invoice ["Location 1"])]
|
||||||
(is (=
|
(is (=
|
||||||
[{:invoice-expense-account/amount -100.0
|
[{:invoice-expense-account/amount -100.0
|
||||||
:invoice-expense-account/location "Location 1"}
|
:invoice-expense-account/location "Location 1"}
|
||||||
|
|||||||
559
test/clj/auto_ap/ssr/ledger_test.clj
Normal file
559
test/clj/auto_ap/ssr/ledger_test.clj
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
(ns auto-ap.ssr.ledger-test
|
||||||
|
(:require
|
||||||
|
[auto-ap.datomic :refer [conn audit-transact transact-schema install-functions]]
|
||||||
|
[auto-ap.datomic.accounts :as a]
|
||||||
|
[auto-ap.integration.util :refer [wrap-setup test-client test-vendor test-bank-account test-account
|
||||||
|
setup-test-data admin-token]]
|
||||||
|
[auto-ap.ssr.ledger :as sut]
|
||||||
|
[auto-ap.ssr.utils :refer [main-transformer]]
|
||||||
|
[auto-ap.ssr.ledger.common :as common]
|
||||||
|
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||||
|
[clj-time.core :as t]
|
||||||
|
[clj-time.coerce :as coerce]
|
||||||
|
[clojure.data.csv :as csv]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[datomic.api :as dc]
|
||||||
|
[malli.core :as mc]))
|
||||||
|
|
||||||
|
(use-fixtures :each wrap-setup)
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Pure Functions - trim-header, tsv->import-data, line->id, entry-errors,
|
||||||
|
;; flatten-errors, entry-error-types
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest trim-header-test
|
||||||
|
(testing "Should trim header with expected keys - returns rest when header matches"
|
||||||
|
(is (= [["data1" "data2" "data3"]]
|
||||||
|
(sut/trim-header [["id" "client" "date"] ["data1" "data2" "data3"]]))))
|
||||||
|
(testing "Should trim header when all columns match"
|
||||||
|
(is (= [["1" "TEST" "2021-01-01" "100.0"]]
|
||||||
|
(sut/trim-header [["id" "client" "date" "debit"] ["1" "TEST" "2021-01-01" "100.0"]]))))
|
||||||
|
(testing "Should not trim header when no columns match"
|
||||||
|
(is (= [["random" "header" "here"]]
|
||||||
|
(sut/trim-header [["random" "header" "here"]])))))
|
||||||
|
|
||||||
|
(deftest tsv->import-data-test
|
||||||
|
(testing "Should parse TSV data from string"
|
||||||
|
(let [data "external-id\tclient-code\tdate\n1\tTEST\t2021-01-01"
|
||||||
|
result (sut/tsv->import-data data)]
|
||||||
|
(is (vector? result))
|
||||||
|
(is (= 1 (count result)))
|
||||||
|
;; tsv->import-data returns vectors of vectors, not maps
|
||||||
|
(is (= "1" (first (first result))))
|
||||||
|
(is (= "TEST" (second (first result))))))
|
||||||
|
(testing "Should return parsed vector when data is already vector"
|
||||||
|
(let [data [{:external-id "1" :client-code "TEST"}]
|
||||||
|
result (sut/tsv->import-data data)]
|
||||||
|
(is (= 1 (count result)))
|
||||||
|
(is (= {:external-id "1" :client-code "TEST"} (first result))))))
|
||||||
|
|
||||||
|
(deftest line->id-test
|
||||||
|
(testing "Should create unique id from source, client-code, and external-id"
|
||||||
|
(is (= "TEST-source-123"
|
||||||
|
(sut/line->id {:source "source"
|
||||||
|
:client-code "TEST"
|
||||||
|
:external-id "123"}))))
|
||||||
|
(testing "Should produce consistent id for same inputs"
|
||||||
|
(let [id1 (sut/line->id {:source "source" :client-code "TEST" :external-id "123"})
|
||||||
|
id2 (sut/line->id {:source "source" :client-code "TEST" :external-id "123"})]
|
||||||
|
(is (= id1 id2)))))
|
||||||
|
|
||||||
|
(deftest entry-errors-test
|
||||||
|
(testing "Should return entry errors concatenated with line-item errors"
|
||||||
|
(let [entry {:errors [["client not found" :error]]
|
||||||
|
:line-items [{:errors [["invalid account" :error]]}
|
||||||
|
{:errors [["amount is zero" :warn]]}]}
|
||||||
|
errors (sut/entry-errors entry)]
|
||||||
|
(is (= 3 (count errors)))
|
||||||
|
(is (contains? (set errors) ["client not found" :error]))
|
||||||
|
(is (contains? (set errors) ["invalid account" :error]))
|
||||||
|
(is (contains? (set errors) ["amount is zero" :warn])))))
|
||||||
|
|
||||||
|
(deftest flatten-errors-test
|
||||||
|
(testing "Should flatten entry and line-item errors into flat list"
|
||||||
|
(let [entries [{:errors [["entry error" :error]]
|
||||||
|
:indices [0 1]
|
||||||
|
:line-items [{:errors [["line error" :error]] :index 0}]}
|
||||||
|
{:errors [["entry warning" :warn]]
|
||||||
|
:indices [2]
|
||||||
|
:line-items [{:errors [["line warning" :warn]] :index 1}]}]
|
||||||
|
errors (sut/flatten-errors entries)]
|
||||||
|
(is (= 5 (count errors)))
|
||||||
|
;; Check that all expected errors are present in the result
|
||||||
|
(is (some #(= [[:table 0] "entry error" :error] %) errors))
|
||||||
|
(is (some #(= [[:table 1] "entry error" :error] %) errors))
|
||||||
|
(is (some #(= [[:table 0] "line error" :error] %) errors))
|
||||||
|
(is (some #(= [[:table 2] "entry warning" :warn] %) errors))
|
||||||
|
(is (some #(= [[:table 1] "line warning" :warn] %) errors))))
|
||||||
|
(testing "Should return empty sequence for no errors"
|
||||||
|
(is (empty? (sut/flatten-errors [])))))
|
||||||
|
|
||||||
|
(deftest entry-error-types-test
|
||||||
|
(testing "Should return set of error types from entry errors"
|
||||||
|
(let [entry {:errors [["client not found" :error]
|
||||||
|
["date is invalid" :error]
|
||||||
|
["warn message" :warn]]}
|
||||||
|
types (sut/entry-error-types entry)]
|
||||||
|
(is (= #{:error :warn} types)))))
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Validation - add-errors
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest add-errors-test
|
||||||
|
(testing "Should add error when client not found"
|
||||||
|
(let [entry {:client-code "NONEXISTENT"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:line-items [{:account-code "1100" :debit 100.0 :credit 0.0}]}
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{} ; all-vendors
|
||||||
|
#{} ; all-accounts
|
||||||
|
{} ; client-locked-lookup (empty = client not found)
|
||||||
|
{} ; all-client-bank-accounts
|
||||||
|
{})] ; all-client-locations
|
||||||
|
(is (= ["Client 'NONEXISTENT' not found." :error]
|
||||||
|
(first (:errors result))))))
|
||||||
|
|
||||||
|
(testing "Should add error when vendor not found"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "CLIENT123"
|
||||||
|
:client/locations ["HQ"])])
|
||||||
|
entry {:client-code "CLIENT123"
|
||||||
|
:vendor-name "NONEXISTENT"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:line-items [{:account-code "1100" :debit 100.0 :credit 0.0 :location "HQ"}]}
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{} ; all-vendors - vendor not present
|
||||||
|
#{"1100"} ; all-accounts
|
||||||
|
{"CLIENT123" #inst "2000-01-01"} ; client-locked-lookup
|
||||||
|
{} ; all-client-bank-accounts
|
||||||
|
{"CLIENT123" #{"HQ"}})] ; all-client-locations
|
||||||
|
(is (= ["Vendor 'NONEXISTENT' not found." :error]
|
||||||
|
(first (:errors result))))))
|
||||||
|
|
||||||
|
(testing "Should add error when client data is locked"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "LOCKED"
|
||||||
|
:client/locked-until #inst "2099-12-31")])
|
||||||
|
entry {:client-code "LOCKED"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2020-01-01") ; before locked date
|
||||||
|
:line-items [{:account-code "1100" :debit 100.0 :credit 0.0}]}
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{"Vendor" {:db/id "vendor-1"}} ; all-vendors
|
||||||
|
#{"1100"} ; all-accounts
|
||||||
|
{"LOCKED" #inst "2099-12-31"} ; client-locked-lookup
|
||||||
|
{} ; all-client-bank-accounts
|
||||||
|
{})] ; all-client-locations
|
||||||
|
(is (some #(str/includes? (first %) "locked") (:errors result)))))
|
||||||
|
|
||||||
|
(testing "Should add error when debit and credit don't balance"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "CLIENT123"
|
||||||
|
:client/locations ["HQ"])])
|
||||||
|
entry {:client-code "CLIENT123"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:amount 150.0
|
||||||
|
:line-items [{:account-code "1100" :debit 100.0 :credit 0.0 :location "HQ"}
|
||||||
|
{:account-code "1101" :debit 0.0 :credit 50.0 :location "HQ"}]}
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{"Vendor" {:db/id "vendor-1"}} ; all-vendors
|
||||||
|
#{"1100" "1101"} ; all-accounts
|
||||||
|
{"CLIENT123" #inst "2000-01-01"} ; client-locked-lookup
|
||||||
|
{} ; all-client-bank-accounts
|
||||||
|
{"CLIENT123" #{"HQ"}})] ; all-client-locations
|
||||||
|
(is (some #(str/includes? (first %) "do not add up") (:errors result)))))
|
||||||
|
|
||||||
|
(testing "Should add warning when line-item amount is zero or negative"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "CLIENT123"
|
||||||
|
:client/locations ["HQ"])])
|
||||||
|
entry {:client-code "CLIENT123"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:amount 100.0
|
||||||
|
:line-items [{:account-code "1100" :debit 0.0 :credit 0.0 :location "HQ"}]}
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{"Vendor" {:db/id "vendor-1"}} ; all-vendors
|
||||||
|
#{"1100"} ; all-accounts
|
||||||
|
{"CLIENT123" #inst "2000-01-01"} ; client-locked-lookup
|
||||||
|
{} ; all-client-bank-accounts
|
||||||
|
{"CLIENT123" #{"HQ"}})] ; all-client-locations
|
||||||
|
;; The entry itself has a warning about $0 total, check line item errors
|
||||||
|
(is (some #(= ["Line item amount 0.0 must be greater than 0." :warn] %)
|
||||||
|
(mapcat :errors (:line-items result))))))
|
||||||
|
|
||||||
|
(testing "Should add error when account not found"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "CLIENT123"
|
||||||
|
:client/locations ["HQ"])])
|
||||||
|
entry {:client-code "CLIENT123"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:line-items [{:account-code 99999 :debit 100.0 :credit 0.0 :location "HQ"}]}
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{"Vendor" {:db/id "vendor-1"}} ; all-vendors
|
||||||
|
#{"1100"} ; all-accounts - 99999 not present
|
||||||
|
{"CLIENT123" #inst "2000-01-01"} ; client-locked-lookup
|
||||||
|
{} ; all-client-bank-accounts
|
||||||
|
{"CLIENT123" #{"HQ"}})] ; all-client-locations
|
||||||
|
(is (some #(str/includes? (first %) "Account '99999' not found")
|
||||||
|
(mapcat :errors (:line-items result))))))
|
||||||
|
|
||||||
|
(testing "Should add error when bank account not found"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "CLIENT123"
|
||||||
|
:client/locations ["HQ"]
|
||||||
|
:client/bank-accounts [(test-bank-account :db/id "bank-1"
|
||||||
|
:bank-account/code "CLIENT123-999")])])
|
||||||
|
entry {:client-code "CLIENT123"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:line-items [{:account-code "CLIENT123-123" :debit 100.0 :credit 0.0 :location "HQ"}]}
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{"Vendor" {:db/id "vendor-1"}} ; all-vendors
|
||||||
|
#{} ; all-accounts
|
||||||
|
{"CLIENT123" #inst "2000-01-01"} ; client-locked-lookup
|
||||||
|
{"CLIENT123" #{"CLIENT123-999"}} ; all-client-bank-accounts - 123 not present
|
||||||
|
{"CLIENT123" #{"HQ"}})] ; all-client-locations
|
||||||
|
(is (some #(str/includes? (first %) "Bank Account 'CLIENT123-123' not found")
|
||||||
|
(mapcat :errors (:line-items result))))))
|
||||||
|
|
||||||
|
(testing "Should add error when location not found"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "CLIENT123"
|
||||||
|
:client/locations ["HQ"])])
|
||||||
|
entry {:client-code "CLIENT123"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:line-items [{:account-code "1100" :debit 100.0 :credit 0.0
|
||||||
|
:location "XX"}]} ; XX is not a valid location
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{"Vendor" {:db/id "vendor-1"}} ; all-vendors
|
||||||
|
#{"1100"} ; all-accounts
|
||||||
|
{"CLIENT123" #inst "2000-01-01"} ; client-locked-lookup
|
||||||
|
{} ; all-client-bank-accounts
|
||||||
|
{"CLIENT123" #{"HQ"}})] ; all-client-locations - XX not present
|
||||||
|
(is (some #(str/includes? (first %) "Location 'XX' not found")
|
||||||
|
(mapcat :errors (:line-items result))))))
|
||||||
|
|
||||||
|
(testing "Should pass through when all validations pass"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "CLIENT123"
|
||||||
|
:client/locations ["HQ"])
|
||||||
|
{:db/id "pass-account-1100"
|
||||||
|
:account/numeric-code 1100
|
||||||
|
:account/account-set "default"
|
||||||
|
:account/name "Cash"}])
|
||||||
|
entry {:client-code "CLIENT123"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:amount 100.0
|
||||||
|
:line-items [{:account-code 1100 :debit 100.0 :credit 100.0 :location "HQ"}]}
|
||||||
|
result (sut/add-errors entry
|
||||||
|
{"Vendor" {:db/id "vendor-1"}} ; all-vendors
|
||||||
|
#{"1100"} ; all-accounts
|
||||||
|
{"CLIENT123" #inst "2000-01-01"} ; client-locked-lookup
|
||||||
|
{} ; all-client-bank-accounts
|
||||||
|
{"CLIENT123" #{"HQ"}})] ; all-client-locations
|
||||||
|
(is (empty? (:errors result)))
|
||||||
|
(is (empty? (mapcat :errors (:line-items result)))))))
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Transaction Building - entry->tx
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest entry->tx-test
|
||||||
|
(testing "Should create upsert transaction for valid entry"
|
||||||
|
(let [vendors {"Vendor" {:db/id "vendor-1" :vendor/name "Vendor"}}
|
||||||
|
entry {:source "manual"
|
||||||
|
:client-code "CLIENT123"
|
||||||
|
:external-id "ext-1"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:amount 100.0
|
||||||
|
:note "Test note"
|
||||||
|
:line-items [{:account-code "1100" :debit 100.0 :credit 0.0
|
||||||
|
:location "HQ"}]}
|
||||||
|
tx (sut/entry->tx entry vendors)]
|
||||||
|
(is (= :upsert-ledger (first tx)))
|
||||||
|
(is (= "manual" (get-in (second tx) [:journal-entry/source])))
|
||||||
|
(is (= [:client/code "CLIENT123"] (get-in (second tx) [:journal-entry/client])))
|
||||||
|
(is (= "ext-1" (get-in (second tx) [:journal-entry/external-id])))
|
||||||
|
(is (= 100.0 (get-in (second tx) [:journal-entry/amount])))
|
||||||
|
(is (= "Test note" (get-in (second tx) [:journal-entry/note])))
|
||||||
|
(is (true? (get-in (second tx) [:journal-entry/cleared])))
|
||||||
|
(is (= 1 (count (get-in (second tx) [:journal-entry/line-items]))))
|
||||||
|
(is (= "HQ" (get-in (second tx) [:journal-entry/line-items 0 :journal-entry-line/location])))
|
||||||
|
(is (= 100.0 (get-in (second tx) [:journal-entry/line-items 0 :journal-entry-line/debit])))
|
||||||
|
(is (= [:bank-account/code "1100"] (get-in (second tx) [:journal-entry/line-items 0 :journal-entry-line/account])))))
|
||||||
|
|
||||||
|
(testing "Should use bank-account code when no matching account"
|
||||||
|
(let [vendors {"Vendor" {:db/id "vendor-1"}}
|
||||||
|
entry {:client-code "TEST"
|
||||||
|
:external-id "ext-2"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:amount 50.0
|
||||||
|
:line-items [{:account-code "TEST-BANK" :debit 50.0 :credit 0.0
|
||||||
|
:location "HQ"}]}
|
||||||
|
tx (sut/entry->tx entry vendors)]
|
||||||
|
(is (-> tx
|
||||||
|
last
|
||||||
|
:journal-entry/line-items
|
||||||
|
(-> first
|
||||||
|
(get :journal-entry-line/account)
|
||||||
|
(= [:bank-account/code "TEST-BANK"]))))))
|
||||||
|
|
||||||
|
(testing "Should skip zero debit"
|
||||||
|
(let [vendors {"Vendor" {:db/id "vendor-1"}}
|
||||||
|
entry {:client-code "TEST"
|
||||||
|
:external-id "ext-3"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:amount 100.0
|
||||||
|
:line-items [{:account-code "1100" :debit 0.0 :credit 100.0
|
||||||
|
:location "HQ"}]}
|
||||||
|
tx (sut/entry->tx entry vendors)]
|
||||||
|
(is (empty? (filter #(= 0.0 (:journal-entry-line/debit %))
|
||||||
|
(-> tx last :journal-entry/line-items))))))
|
||||||
|
|
||||||
|
(testing "Should skip zero credit"
|
||||||
|
(let [vendors {"Vendor" {:db/id "vendor-1"}}
|
||||||
|
entry {:client-code "TEST"
|
||||||
|
:external-id "ext-4"
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:amount 100.0
|
||||||
|
:line-items [{:account-code "1100" :debit 100.0 :credit 0.0
|
||||||
|
:location "HQ"}]}
|
||||||
|
tx (sut/entry->tx entry vendors)]
|
||||||
|
(is (empty? (filter #(= 0.0 (:journal-entry-line/credit %))
|
||||||
|
(-> tx last :journal-entry/line-items)))))))
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Import Flow - table->entries, import-ledger
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest table->entries-test
|
||||||
|
(testing "Should group lines with same line->id and add errors"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "client-1"
|
||||||
|
:client/code "TEST"
|
||||||
|
:client/locations ["HQ"])])
|
||||||
|
table [{:source "manual"
|
||||||
|
:client-code "TEST"
|
||||||
|
:external-id "ext-1"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:debit 100.0
|
||||||
|
:credit 0.0
|
||||||
|
:account-code "1100"
|
||||||
|
:location "HQ"}
|
||||||
|
{:source "manual"
|
||||||
|
:client-code "TEST"
|
||||||
|
:external-id "ext-1"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:debit 50.0
|
||||||
|
:credit 150.0
|
||||||
|
:account-code "1101"
|
||||||
|
:location "HQ"}]
|
||||||
|
vendors {"Vendor" {:db/id "vendor-1"}}
|
||||||
|
entries (sut/table->entries table
|
||||||
|
vendors
|
||||||
|
#{"1100" "1101"} ; all-accounts
|
||||||
|
{"TEST" #inst "2000-01-01"} ; client-locked-lookup
|
||||||
|
{"TEST" #{}} ; all-client-bank-accounts
|
||||||
|
{"TEST" #{"HQ"}})] ; all-client-locations
|
||||||
|
(is (= 1 (count entries)))
|
||||||
|
(is (= "TEST" (:client-code (first entries))))
|
||||||
|
(is (= 2 (count (:line-items (first entries))))))))
|
||||||
|
|
||||||
|
(deftest import-ledger-test
|
||||||
|
(testing "Should upsert hidden vendors and create transactions"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "import-client-1"
|
||||||
|
:client/code "IMPORT-TEST"
|
||||||
|
:client/locations ["HQ"])
|
||||||
|
{:db/id "import-account-1100"
|
||||||
|
:account/numeric-code 1100
|
||||||
|
:account/account-set "default"
|
||||||
|
:account/name "Cash"}])
|
||||||
|
form-params {:table [{:source "manual"
|
||||||
|
:client-code "IMPORT-TEST"
|
||||||
|
:external-id "ext-import-unique-1"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:vendor-name "New Vendor Import Unique"
|
||||||
|
:debit 100.0
|
||||||
|
:credit 100.0
|
||||||
|
:account-code 1100
|
||||||
|
:location "HQ"}]}
|
||||||
|
admin-identity (admin-token)
|
||||||
|
result (sut/import-ledger {:form-params form-params
|
||||||
|
:identity admin-identity})
|
||||||
|
db-after (dc/db conn)]
|
||||||
|
(is (= 1 (:successful result)))
|
||||||
|
;; Verify vendor was upserted as hidden
|
||||||
|
(let [vendor-id (dc/q '[:find ?e .
|
||||||
|
:where [?e :vendor/name "New Vendor Import Unique"]]
|
||||||
|
db-after)]
|
||||||
|
(is vendor-id)))))
|
||||||
|
|
||||||
|
(deftest import-ledger-with-errors-test
|
||||||
|
(testing "Should throw exception when entries have errors - client not found"
|
||||||
|
(let [form-params {:table [{:source "manual"
|
||||||
|
:client-code "NONEXISTENT-ERR"
|
||||||
|
:external-id "ext-err-1"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:vendor-name "Vendor"
|
||||||
|
:debit 100.0
|
||||||
|
:credit 0.0
|
||||||
|
:account-code 1100
|
||||||
|
:location "HQ"}]}
|
||||||
|
admin-identity (admin-token)]
|
||||||
|
(is (thrown? Exception (sut/import-ledger {:form-params form-params
|
||||||
|
:identity admin-identity})))))
|
||||||
|
|
||||||
|
(testing "Should produce form-errors for invalid account entries"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "err-client-1"
|
||||||
|
:client/code "ERR-TEST"
|
||||||
|
:client/locations ["HQ"])
|
||||||
|
(test-vendor :db/id "err-vendor-1"
|
||||||
|
:vendor/name "Err Vendor")])
|
||||||
|
form-params {:table [{:source "manual"
|
||||||
|
:client-code "ERR-TEST"
|
||||||
|
:external-id "ext-err-2"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:vendor-name "Err Vendor"
|
||||||
|
:debit 100.0
|
||||||
|
:credit 100.0
|
||||||
|
:account-code 99999
|
||||||
|
:location "HQ"}]}
|
||||||
|
admin-identity (admin-token)]
|
||||||
|
(is (thrown? Exception (sut/import-ledger {:form-params form-params
|
||||||
|
:identity admin-identity}))))))
|
||||||
|
|
||||||
|
(deftest import-ledger-with-warnings-test
|
||||||
|
(testing "Should ignore entries with only warnings"
|
||||||
|
(let [_ (setup-test-data [(test-client :db/id "warn-client-1"
|
||||||
|
:client/code "WARN-CLIENT"
|
||||||
|
:client/locations ["HQ"])
|
||||||
|
(test-vendor :db/id "warn-vendor-1"
|
||||||
|
:vendor/name "Warn Vendor")
|
||||||
|
{:db/id "warn-account-1100"
|
||||||
|
:account/numeric-code 1100
|
||||||
|
:account/account-set "default"
|
||||||
|
:account/name "Cash"}])
|
||||||
|
client-code "WARN-CLIENT"
|
||||||
|
form-params {:table [{:source "manual"
|
||||||
|
:client-code client-code
|
||||||
|
:external-id "ext-warn-unique-1"
|
||||||
|
:date (coerce/to-date-time #inst "2021-01-01")
|
||||||
|
:vendor-name "Warn Vendor"
|
||||||
|
:debit 0.0
|
||||||
|
:credit 0.0
|
||||||
|
:account-code 1100
|
||||||
|
:location "HQ"}]}
|
||||||
|
admin-identity (admin-token)
|
||||||
|
result (sut/import-ledger {:form-params form-params
|
||||||
|
:identity admin-identity})]
|
||||||
|
;; Entries with only warnings are ignored, not imported
|
||||||
|
(is (= 0 (:successful result)))
|
||||||
|
(is (= 1 (:ignored result))))))
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Selected IDs - selected->ids
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest selected->ids-test
|
||||||
|
(testing "Should return all ids when all-selected is true"
|
||||||
|
(with-redefs [common/fetch-ids (constantly {:ids ["id1" "id2" "id3"]})]
|
||||||
|
(let [request {:query-params {:all-selected true :start 0 :per-page 10}}
|
||||||
|
result (sut/selected->ids request {:all-selected true})]
|
||||||
|
(is (= #{"id1" "id2" "id3"} (set result))))))
|
||||||
|
|
||||||
|
(testing "Should return selected ids when all-selected is false"
|
||||||
|
(is (= #{"id1" "id2"} (set (sut/selected->ids {} {:all-selected false
|
||||||
|
:selected ["id1" "id2"]})))))
|
||||||
|
|
||||||
|
(testing "Should return empty set when no selection"
|
||||||
|
(is (empty? (sut/selected->ids {} {})))))
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Bank Account Middleware
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest wrap-ensure-bank-account-belongs-test
|
||||||
|
(testing "Should remove bank-account when client not present"
|
||||||
|
(let [handler (fn [req] req)
|
||||||
|
wrapped (sut/wrap-ensure-bank-account-belongs handler)
|
||||||
|
request {:query-params {:bank-account {:db/id "bank-1"}}
|
||||||
|
:identity (admin-token)}]
|
||||||
|
(is (nil? (-> (wrapped request) :query-params :bank-account)))))
|
||||||
|
|
||||||
|
(testing "Should remove bank-account when client doesn't own it"
|
||||||
|
(let [handler (fn [req] req)
|
||||||
|
wrapped (sut/wrap-ensure-bank-account-belongs handler)
|
||||||
|
request {:query-params {:bank-account {:db/id "bank-1"}}
|
||||||
|
:client {:client/bank-accounts []}
|
||||||
|
:identity (admin-token)}]
|
||||||
|
(is (nil? (-> (wrapped request) :query-params :bank-account)))))
|
||||||
|
|
||||||
|
(testing "Should keep bank-account when client owns it"
|
||||||
|
(let [handler (fn [req] req)
|
||||||
|
wrapped (sut/wrap-ensure-bank-account-belongs handler)
|
||||||
|
request {:query-params {:bank-account {:db/id "bank-1"}}
|
||||||
|
:client {:client/bank-accounts [{:db/id "bank-1"}]}
|
||||||
|
:identity (admin-token)}]
|
||||||
|
(is (= "bank-1" (-> (wrapped request) :query-params :bank-account :db/id))))))
|
||||||
|
|
||||||
|
(deftest parse-form-schema-test
|
||||||
|
(testing "Should parse valid TSV data"
|
||||||
|
(let [tsv "id\tclient\tsource\tvendor\tdate\taccount\tlocation\tdebit\tcredit\n1\tTEST\tmanual\tVendor\t2021-01-01\t1100\tHQ\t100.0\t0.0"
|
||||||
|
result (mc/decode sut/parse-form-schema
|
||||||
|
{:table tsv}
|
||||||
|
main-transformer)]
|
||||||
|
(is (vector? (:table result)))
|
||||||
|
(is (= 1 (count (:table result))))))
|
||||||
|
(testing "Should not validate for invalid date"
|
||||||
|
;; mc/decode doesn't throw - it returns invalid data that fails validation
|
||||||
|
;; Need to add a data row (header gets trimmed), so use two rows
|
||||||
|
(let [result (mc/decode sut/parse-form-schema
|
||||||
|
{:table "id\tclient\tsource\tvendor\tdate\taccount\tlocation\tdebit\tcredit\n1\tTEST\tmanual\tVendor\tinvalid-date\t1100\tHQ\t100.0\t0.0"}
|
||||||
|
main-transformer)]
|
||||||
|
;; The result should fail validation due to invalid date
|
||||||
|
(is (not (mc/validate sut/parse-form-schema result))))))
|
||||||
|
|
||||||
|
;; =============================================================================
|
||||||
|
;; Helper Functions
|
||||||
|
;; =============================================================================
|
||||||
|
|
||||||
|
(deftest delete-invoice-test
|
||||||
|
(testing "Should throw notification exception when trying to void a paid invoice"
|
||||||
|
(is (thrown? Exception (sut/delete {:entity {:invoice/status :invoice-status/paid
|
||||||
|
:invoice/payments []}
|
||||||
|
:identity (admin-token)}))))
|
||||||
|
|
||||||
|
(testing "Should throw notification exception when trying to void invoice with payments"
|
||||||
|
(is (thrown? Exception (sut/delete {:entity {:invoice/status :invoice-status/unpaid
|
||||||
|
:invoice/payments [{:payment/status :payment-status/cleared}]}
|
||||||
|
:identity (admin-token)}))))
|
||||||
|
|
||||||
|
(testing "Should void unpaid invoice with no payments"
|
||||||
|
(let [tempids (setup-test-data [(test-client :db/id "del-client-1"
|
||||||
|
:client/code "TESTCLIENT-DEL")])
|
||||||
|
client-id (get tempids "del-client-1")]
|
||||||
|
(with-redefs [auto-ap.graphql.utils/assert-can-see-client (fn [_ _] true)]
|
||||||
|
(let [temp-id (str (java.util.UUID/randomUUID))
|
||||||
|
tx-result @(dc/transact conn [{:db/id temp-id
|
||||||
|
:invoice/status :invoice-status/unpaid
|
||||||
|
:invoice/total 100.0
|
||||||
|
:invoice/outstanding-balance 100.0
|
||||||
|
:invoice/client client-id}])
|
||||||
|
invoice-id (get-in tx-result [:tempids temp-id])]
|
||||||
|
(let [response (sut/delete {:entity {:db/id invoice-id
|
||||||
|
:invoice/status :invoice-status/unpaid
|
||||||
|
:invoice/payments []
|
||||||
|
:invoice/client {:db/id client-id}}
|
||||||
|
:identity (admin-token)})]
|
||||||
|
(is (= (format "#entity-table tr[data-id=\"%d\"]" invoice-id)
|
||||||
|
(get-in response [:headers "hx-retarget"])))))))))
|
||||||
Reference in New Issue
Block a user