Going on implementing our toy digital bank, as an attempt to get hands-on experience in Clojure REPL driven development (RDD), we will now tackle its last endpoint: transaction create.
It is handled by means of an HTTP POST. We deviced its JSON body structure according to transaction type. In all of them, amount must be a double
.
We will implement de following transactions:
Positive amount
, no account
(attribute exclusive for transfers
)
{
"amount": 1000.00,
"description": "appartment rent - march 2021"
}
Negative amount
{
"amount": -1000.00,
"description": "appartment rent - march 2021"
}
Negative amount
, setting target account’s id in account
{
"amount": -1000.00,
"account": "account-1",
"description": "appartment rent - march 2021"
}
Although in this body there is no explicit indication on transation type, each of them can be distinguished by the following criteria:
:account
attribute is present, transaction is a transfer
; otherwise, it is a deposit
or withdrawal
deposit
and withdrawal
takes place just in its amount
sign: positive for deposits
, and viceversatransfer
transactions are basically splitted into a deposit
into the target account, and a withdrawal
from the source one.
To actually handle each of these transactions, several interceptors has two versions, carrying the following sufixes, usually delegating its actual implementation into a common function, with type
as the identifying parameter:
credit
: when money is put into an accountdebit
: when it is taken from itWe will first address the data structure deviced for data handling along interceptor chain execution for this endpoint. And then, we will explain those interceptors.
The only key new in this endpoint is :tx-data
: prepare-update
interceptors leave data there, “prepared” for update
interceptors to actually run the corresponding Datomic transaction:
{:request
{:path-params {:account-id "account-1"}
:json-params {:amount 1000.0 :description "test"}}
:query-data {:debit {:id "account-1"}}
:retrieved {:accounts {:credit #:account{:id "account-1", :balance 10000.0}}}
:tx-data
{:credit
{:id "account-1"
:new-balance 11000.0
:tx {:amount 1000.0 :description "test" :balance 11000.0}}}
:result {:amount 1000.0 :description "test" :balance 11000.0}}
We will now explain interceptors bound to this endpoint. As it has associated much more than the other endpoints, we will list and briefly describe them below.
We may group them the following way:
In these interceptors, data is prepared to be handled in the following interceptor set, update
, in which they will be transacted against our Datomic database. Then, it will be left in data structure’s :tx-data
field.
These interceptors prepare the following entities:
Each of these two entities is taken from data structures’s :tx-data
entry, and then transacted against our Datomic database.
Transaction just created is left in data structures’s :result
in order to have it ready for the entity-render
interceptor to set it in response
.
We have just finished the project !
In order to perform the final e2e testing, let’s switch to main
branch.
We will:
account-1
account-1
to account-2
Immediately before and after each of these transactions, we query database on each account involved in the transaction as well as that account’s transaction log.
user> (start)
{:started
["#'accounts.conf/config"
"#'accounts.db.conn/conn"
"#'accounts.web.server/server"]}
user> (c/load-database)
{:db-before datomic.db.Db@ae9a6f92,
:db-after datomic.db.Db@a1ccb95e,
:tx-data
[#datom[13194139534332 50 #inst "2021-05-07T10:03:22.467-00:00" 13194139534332 true] #datom[17592186045418 64 10000.0 13194139534332 true] #datom[17592186045418 64 9000.0 13194139534332 false] #datom[17592186045419 64 20000.0 13194139534332 true] #datom[17592186045419 64 19000.0 13194139534332 false] #datom[17592186045420 64 26000.0 13194139534332 true] #datom[17592186045420 64 28000.0 13194139534332 false]],
:tempids {}}
user> (q/pull-account-by-id "account-1")
#:account{:id "account-1", :balance 10000.0}
user> (q/pull-transactions-by-account-id "account-1")
({:db/id 17592186045435,
:transaction/id "trx-10",
:transaction/amount 1000.0,
:transaction/description "thomas' present",
:transaction/transfer-account-id #:db{:id 17592186045419},
:transaction/balance 10000.0}
{:db/id 17592186045427,
:transaction/id "trx-4",
:transaction/amount -1000.0,
:transaction/description "appartment rent - febr 2021",
:transaction/balance 9000.0}
{:db/id 17592186045422,
:transaction/id "trx-1",
:transaction/amount 10000.0,
:transaction/description "first deposit",
:transaction/balance 10000.0})
user> (deposit-1)
{:status 200,
:body
"{\"amount\":2000.0,\"description\":\"second deposit\",\"balance\":12000.0}",
:headers
{"Strict-Transport-Security" "max-age=31536000; includeSubdomains",
"X-Frame-Options" "DENY",
"X-Content-Type-Options" "nosniff",
"X-XSS-Protection" "1; mode=block",
"X-Download-Options" "noopen",
"X-Permitted-Cross-Domain-Policies" "none",
"Content-Security-Policy"
"object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;",
"Content-Type" "application/json;charset=UTF-8"}}
user> (q/pull-account-by-id "account-1")
#:account{:id "account-1", :balance 12000.0}
user> (q/pull-transactions-by-account-id "account-1")
({:db/id 17592186045439,
:transaction/id "trx-930809",
:transaction/amount 2000.0,
:transaction/description "second deposit",
:transaction/balance 12000.0}
{:db/id 17592186045435,
:transaction/id "trx-10",
:transaction/amount 1000.0,
:transaction/description "thomas' present",
:transaction/transfer-account-id #:db{:id 17592186045419},
:transaction/balance 10000.0}
{:db/id 17592186045427,
:transaction/id "trx-4",
:transaction/amount -1000.0,
:transaction/description "appartment rent - febr 2021",
:transaction/balance 9000.0}
{:db/id 17592186045422,
:transaction/id "trx-1",
:transaction/amount 10000.0,
:transaction/description "first deposit",
:transaction/balance 10000.0})
user> (withdrawal)
{:status 200,
:body
"{\"amount\":-1000.0,\"description\":\"appartment rent - march 2021\",\"balance\":11000.0}",
:headers
{"Strict-Transport-Security" "max-age=31536000; includeSubdomains",
"X-Frame-Options" "DENY",
"X-Content-Type-Options" "nosniff",
"X-XSS-Protection" "1; mode=block",
"X-Download-Options" "noopen",
"X-Permitted-Cross-Domain-Policies" "none",
"Content-Security-Policy"
"object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;",
"Content-Type" "application/json;charset=UTF-8"}}
user> (q/pull-account-by-id "account-1")
#:account{:id "account-1", :balance 11000.0}
user> (q/pull-transactions-by-account-id "account-1")
({:db/id 17592186045442,
:transaction/id "trx-930817",
:transaction/amount -1000.0,
:transaction/description "appartment rent - march 2021",
:transaction/balance 11000.0}
{:db/id 17592186045439,
:transaction/id "trx-930809",
:transaction/amount 2000.0,
:transaction/description "second deposit",
:transaction/balance 12000.0}
{:db/id 17592186045435,
:transaction/id "trx-10",
:transaction/amount 1000.0,
:transaction/description "thomas' present",
:transaction/transfer-account-id #:db{:id 17592186045419},
:transaction/balance 10000.0}
{:db/id 17592186045427,
:transaction/id "trx-4",
:transaction/amount -1000.0,
:transaction/description "appartment rent - febr 2021",
:transaction/balance 9000.0}
{:db/id 17592186045422,
:transaction/id "trx-1",
:transaction/amount 10000.0,
:transaction/description "first deposit",
:transaction/balance 10000.0})
user> (q/pull-account-by-id "account-2")
#:account{:id "account-2", :balance 20000.0}
user> (q/pull-transactions-by-account-id "account-2")
({:db/id 17592186045434,
:transaction/id "trx-9",
:transaction/amount -1000.0,
:transaction/description "thomas' present",
:transaction/transfer-account-id #:db{:id 17592186045418},
:transaction/balance 20000.0}
{:db/id 17592186045433,
:transaction/id "trx-8",
:transaction/amount 2000.0,
:transaction/description "peter's present",
:transaction/transfer-account-id #:db{:id 17592186045420},
:transaction/balance 26000.0}
{:db/id 17592186045428,
:transaction/id "trx-5",
:transaction/amount -1000.0,
:transaction/description "credit card - febr 2021",
:transaction/balance 19000.0}
{:db/id 17592186045423,
:transaction/id "trx-2",
:transaction/amount 20000.0,
:transaction/description "first deposit",
:transaction/balance 20000.0})
user> (transfer)
{:status 200,
:body
"{\"amount\":-1000.0,\"description\":\"anne's present\",\"account-id\":\"account-2\",\"balance\":10000.0}",
:headers
{"Strict-Transport-Security" "max-age=31536000; includeSubdomains",
"X-Frame-Options" "DENY",
"X-Content-Type-Options" "nosniff",
"X-XSS-Protection" "1; mode=block",
"X-Download-Options" "noopen",
"X-Permitted-Cross-Domain-Policies" "none",
"Content-Security-Policy"
"object-src 'none'; script-src 'unsafe-inline' 'unsafe-eval' 'strict-dynamic' https: http:;",
"Content-Type" "application/json;charset=UTF-8"}}
user> (q/pull-account-by-id "account-1")
#:account{:id "account-1", :balance 10000.0}
user> (q/pull-transactions-by-account-id "account-1")
({:db/id 17592186045446,
:transaction/id "trx-930830",
:transaction/amount -1000.0,
:transaction/description "anne's present",
:transaction/balance 10000.0}
{:db/id 17592186045442,
:transaction/id "trx-930817",
:transaction/amount -1000.0,
:transaction/description "appartment rent - march 2021",
:transaction/balance 11000.0}
{:db/id 17592186045439,
:transaction/id "trx-930809",
:transaction/amount 2000.0,
:transaction/description "second deposit",
:transaction/balance 12000.0}
{:db/id 17592186045435,
:transaction/id "trx-10",
:transaction/amount 1000.0,
:transaction/description "thomas' present",
:transaction/transfer-account-id #:db{:id 17592186045419},
:transaction/balance 10000.0}
{:db/id 17592186045427,
:transaction/id "trx-4",
:transaction/amount -1000.0,
:transaction/description "appartment rent - febr 2021",
:transaction/balance 9000.0}
{:db/id 17592186045422,
:transaction/id "trx-1",
:transaction/amount 10000.0,
:transaction/description "first deposit",
:transaction/balance 10000.0})
user> (q/pull-account-by-id "account-2")
#:account{:id "account-2", :balance 21000.0}
user> (q/pull-transactions-by-account-id "account-2")
({:db/id 17592186045448,
:transaction/id "trx-930831",
:transaction/amount 1000.0,
:transaction/description "anne's present",
:transaction/balance 21000.0}
{:db/id 17592186045434,
:transaction/id "trx-9",
:transaction/amount -1000.0,
:transaction/description "thomas' present",
:transaction/transfer-account-id #:db{:id 17592186045418},
:transaction/balance 20000.0}
{:db/id 17592186045433,
:transaction/id "trx-8",
:transaction/amount 2000.0,
:transaction/description "peter's present",
:transaction/transfer-account-id #:db{:id 17592186045420},
:transaction/balance 26000.0}
{:db/id 17592186045428,
:transaction/id "trx-5",
:transaction/amount -1000.0,
:transaction/description "credit card - febr 2021",
:transaction/balance 19000.0}
{:db/id 17592186045423,
:transaction/id "trx-2",
:transaction/amount 20000.0,
:transaction/description "first deposit",
:transaction/balance 20000.0})
user>
Source code available in my repo, at GitHub
As I am now studying Kubernetes, Kafka and Istio, next post will likely go on working with this restful API in the context of those tools.
As I have been studying Haskell for the last couple years, mainly with the “Haskell Book”, I’d like to implement other APIs, first with Scotty, which is the Web framewok suggested in that book as a first step for API development, and covered extensively there. And then, perhaps, go on wih Servant
Not sure yet which of these alternatives will actually be our next step. First option would definitely be much more convenient for my professional career in the short term. On the other hand, Haskell have meant a deep and revealing enlightment I am eager to adopt in practice. In Technology, short term career is always the most “sensible” reason to keep postponing actual technology marvels…
So… stay tuned !