This repo contains a RESTful service written in Haskell using the scotty
web framework that exposes some basic checking bank account functionality.
Stack
was used for managing the packages, it is a fantastic tool that just works. You can find instructions on how to install here.
After installing Stack
, clone this repo and cd
into the project. In order to build the project, run
# download the compiler if needed
stack setup
# build the project
stack build
That's it! And then when you want to run the service at a specific port:
PORT=3000 stack exec haskell-checking-account-exe
If you want to launch a REPL:
stack ghci
You can run the tests with:
stack test
app/Main.hs
- the entry point for our app. It just callsLib.main
haskell-checking-account.cabal
-cabal
file automatically generated byStack
package.yaml
- the file where we add all the dependencies and project metadatastack.yaml
-Stack
configurationsrc
- contains all the code used by this serviceControllers/
- the service's controllers and routesCore.hs
- some shared code between the controllers and the server to glue everything together. It defines how the environment (memory database) is loaded to the server and defines helper functions to access the environment from the routes.Models/
- the models that our app uses (e.g.Account
,Operation
)Persistence/
- defines the (memory) Database's initial structure and helper functions to extract data from the Database. If in the future we need to replace it with some other persistence method (SQL server?), this is the directory that we'll need to edit.Serializers/
- contains the code that transforms our models into JSON. Alternatively, we could have the models implementing thetoJSON
methods, but that's not a good practice, as one model can be serialized in many different ways, depending on what fields we want to whitelist each time.
test
- our specs, testing all the source code. It mainly contains unit tests, but tests intest/Controllers/
could be considered integration tests, as they test the whole functionality end to end.
A TVar
was used for storing our database. It provides atomic concurrency-safe transactions. It is bootstrapped during the server startup, and helper functions in src/Core.hs
allow it to be accessed in a transactional way.
Creates an account. It requires an owner
String in the body. The account ID returned is needed for all the following requests.
It fails with 400
if there is no owner
passed.
curl -v -X POST http://localhost:3000/accounts -d '{"owner": "kostis"}'
# POST /accounts
# {
# "owner": "kostis"
# }
#
# 200 OK
# {
# "status": "success",
# "data": {
# "owner": "kostis",
# "id": "dfcf2ebe-4923-4b49-ac12-7de0734daa4b",
# "type": "Account"
# }
# }
Gets an account. Nothing too exciting. It returns 404
if the account does not exist.
curl -v http://localhost:3000/accounts/dfcf2ebe-4923-4b49-ac12-7de0734daa4b
# GET /accounts/dfcf2ebe-4923-4b49-ac12-7de0734daa4b
#
# 200 OK
# {
# "status": "success",
# "data": {
# "owner": "kostis",
# "id": "dfcf2ebe-4923-4b49-ac12-7de0734daa4b",
# "type": "Account"
# }
# }
It creates an operation for an account. The operation type can be either credit or debit. amount
always appears as positive. It also requires an accountId
, an ISO8601 date
, and a decription
.
They service supports creating operations in any order, as they are stored in a Set
in the background.
If one of these fields is missing, accountId
does not correspond to a created account, amount
is negative, operationType
is not either "credit"
or "debit"
, it fails with 400
.
curl -v -X POST http://localhost:3000/operations -d '{"accountId": "dfcf2ebe-4923-4b49-ac12-7de0734daa4b", "operationType": "debit", "amount": 1000, "date": "2019-07-21", "description": "airline tickets to New York"}'
# POST /operations
# {
# "accountId": "dfcf2ebe-4923-4b49-ac12-7de0734daa4b",
# "operationType": "debit",
# "amount": 1000,
# "date": "2019-07-21",
# "description": "airline tickets to New York"
# }
#
# 200 OK
# {
# "status": "success",
# "data": {
# "amount": 1000,
# "date": "2019-07-21",
# "accountId": "dfcf2ebe-4923-4b49-ac12-7de0734daa4b",
# "id": "4f2f24bd-5ca5-43b1-8249-f84f6c1bde97",
# "operationType": "debit",
# "type": "Operation",
# "description": "airline tickets to New York"
# }
# }
Retrieve the account's balance. Fails with 404
if there's no account with such ID.
curl -v http://localhost:3000/accounts/dfcf2ebe-4923-4b49-ac12-7de0734daa4b/balance
# GET /accounts/dfcf2ebe-4923-4b49-ac12-7de0734daa4b/balance
#
# 200 OK
# {
# "status": "success",
# "data": -700.0
# }
Get an accounts bank statement. fromDate
and toDate
query params are used to pass ISO8601 date strings that define the start and end (included) of the statement.
If the query params are missing, they're not ISO8601 strings, or fromDate
is greater than toDate
, the request fails with 400
. If there is no account with such ID, it fails with 404
.
curl -v "http://localhost:3000/accounts/dfcf2ebe-4923-4b49-ac12-7de0734daa4b/statement?fromDate=2018-01-01&toDate=2020-01-01"
# GET /accounts/dfcf2ebe-4923-4b49-ac12-7de0734daa4b/statement?fromDate=2018-01-01&toDate=2020-01-01
#
# 200 OK
# {
# "status": "success",
# "data": {
# "fromDate": "2018-01-01",
# "toDate": "2020-01-01",
# "statementDates": [
# {
# "date": "2019-07-18",
# "endOfDayBalance": 100,
# "type": "StatementDate",
# "operations": [
# {
# "amount": 100,
# "date": "2019-07-18",
# "accountId": "dfcf2ebe-4923-4b49-ac12-7de0734daa4b",
# "id": "c8a18269-a212-4b22-8ab6-94e35b13da6c",
# "operationType": "credit",
# "type": "Operation",
# "description": "gift from parents"
# }
# ]
# },
# {
# "date": "2019-07-21",
# "endOfDayBalance": -700.0,
# "type": "StatementDate",
# "operations": [
# {
# "amount": 1000,
# "date": "2019-07-21",
# "accountId": "dfcf2ebe-4923-4b49-ac12-7de0734daa4b",
# "id": "4f2f24bd-5ca5-43b1-8249-f84f6c1bde97",
# "operationType": "debit",
# "type": "Operation",
# "description": "airline tickets to New York"
# },
# {
# "amount": 200,
# "date": "2019-07-21",
# "accountId": "dfcf2ebe-4923-4b49-ac12-7de0734daa4b",
# "id": "df08838f-d6a6-46f7-9780-032e4f68ac36",
# "operationType": "credit",
# "type": "Operation",
# "description": "some deposit"
# }
# ]
# }
# ],
# "type": "Statement"
# }
# }
Returns the all-time periods of debt for the account. Again, it fails with 404
if there there's no account with such ID.
curl -v http://localhost:3000/accounts/dfcf2ebe-4923-4b49-ac12-7de0734daa4b/debtPeriods
# GET /accounts/dfcf2ebe-4923-4b49-ac12-7de0734daa4b/debtPeriods
#
# 200 OK
# {
# "status": "success",
# "data": [
# {
# "fromDate": "2019-07-21",
# "toDate": null,
# "principal": 700.0,
# "type": "DebtPeriod"
# }
# ]
# }
Successful responses have their status
attribute set to "success"
, and the requested resource nested inside data
. Most resources have a type
declaring what this returned resource is, e.g. "Account"
or "DebtPeriod"
.
Error responses have their status
attribute set to "error"
, and a message
attribute explaining what went wrong.
The API was developed in this way because if we wish at some point to add metadata or other data to the response irrelevant to the resource, it will be trivial.