diff --git a/README.md b/README.md
index 45130aa..183999a 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,136 @@
# crypto-wallet
+
+A demo crypto wallet app using with teaching purposes. This app allows clients to manage their crypto currency portfolio. It uses [Coinmarketcap API](https://coinmarketcap.com/api/) for getting the updated cryptocurrencies quotation.
+
+It provides the following functionalities:
+
+* managing users (CRUD operations, uses for admins)
+* buy, sell and transfer crypto currencies
+* allow users to see their portfolio balance (the quotation of their cryptocurrencies portfolio converted to USD)
+* allows users to see their transaction history
+
+## Tech Stack
+
+* Java 11
+* Spring Boot
+* Postgres
+* Kafka
+* Docker
+
+## Architecture
+
+The following diagram shows the systems architecture:
+
+![Alt text](diagrams/cw-architecture.png?raw=true "Architecture")
+
+## Data Model
+
+The following diagram shows the data model:
+
+![Alt text](diagrams/cw-data-model.png?raw=true "Title")
+
+## Run the app
+
+1. Start containers using docker compose:
+
+```
+docker-compose up
+```
+
+2. Run the application
+
+```
+mvn spring-boot:run
+```
+
+It runs the application on http://localhost:8080
+
+## API
+
+* Healthcheck
+
+```
+curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X GET http://localhost:8080/healthcheck
+```
+
+* Get user by id
+
+```
+curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X GET http://localhost:8080/users/:userId
+```
+
+* Get all users
+
+```
+curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X GET http://localhost:8080/users
+```
+
+* Create user
+
+```
+curl -X POST -H "Content-Type: application/json" \
+ -d '{"username": "pepe", "password": "pass1234", "email": "pepe@gmail.com"}' \
+ http://localhost:8080/users
+```
+
+* Update user
+
+```
+curl -X PUT -H "Content-Type: application/json" \
+ -d '{"username": "pepe", "password": "pass1234", "email": "pepe@gmail.com"}' \
+ http://localhost:8080/users/:userId
+```
+
+* Delete user
+
+```
+curl -X "DELETE" http://localhost:8080/users/:userId
+```
+
+* Get quotes
+
+```
+curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X GET http://localhost:8080/cryptocurrencies/quotes
+```
+
+* Get portfolio
+
+```
+curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X GET http://localhost:8080/portfolios/:userId
+```
+
+* Get transaction history
+
+```
+curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X GET http://localhost:8080/transactions/8
+```
+
+* Transfer
+
+```
+curl -X POST -H "Content-Type: application/json" \
+ -d '{"issuer": "88", "receiver": "2", "cryptocurrency": "Huobi Token", "amount": 10}' \
+ http://localhost:8080/transactions/transferences
+```
+
+* Buy
+
+```
+curl -X POST -H "Content-Type: application/json" \
+ -d '{"userId": 3, "cryptocurrency": "Bitcoin", "amountInUsd": 100000}' \
+ http://localhost:8080/transactions/buys
+```
+
+* Sell
+
+```
+curl -X POST -H "Content-Type: application/json" \
+ -d '{"userId": 3, "cryptocurrency": "Bitcoin", "amount": 1}' \
+ http://localhost:8080/transactions/sells
+```
+
+## Extra notes
+
+* A ready [docker-compose](docker-compose.yml) is provided. It contains all the components ready for local development. It also contains dummy data for playing around with the app.
+
+* Also, a [postman collection](postman-collection/crypto.postman_collection.json) is provided.
\ No newline at end of file
diff --git a/diagrams/cw-architecture.png b/diagrams/cw-architecture.png
new file mode 100644
index 0000000..d6f10e8
Binary files /dev/null and b/diagrams/cw-architecture.png differ
diff --git a/diagrams/cw-data-model.png b/diagrams/cw-data-model.png
new file mode 100644
index 0000000..7f6eba1
Binary files /dev/null and b/diagrams/cw-data-model.png differ
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..72f1a60
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,44 @@
+version: '3.7'
+
+services:
+
+ cw-postgres:
+ image: postgres:latest
+ environment:
+ - "TZ=Europe/Amsterdam"
+ - "POSTGRES_USER=root"
+ - "POSTGRES_PASSWORD=root"
+ - "POSTGRES_DB=cryptodb"
+ ports:
+ - 45432:5432
+ volumes:
+ - ./sql:/docker-entrypoint-initdb.d
+
+ cw-adminer:
+ image: adminer
+ restart: always
+ ports:
+ - 8083:8080
+
+ cw-zookeeper:
+ image: wurstmeister/zookeeper
+ ports:
+ - 2181:2181
+
+ cw-kafka:
+ image: wurstmeister/kafka
+ ports:
+ - 9092:9092
+ environment:
+ KAFKA_BROKER_ID: 0
+ KAFKA_ZOOKEEPER_CONNECT: cw-zookeeper:2181
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: INSIDE:PLAINTEXT,OUTSIDE:PLAINTEXT
+ KAFKA_ADVERTISED_LISTENERS: INSIDE://localhost:19092,OUTSIDE://localhost:9092
+ KAFKA_LISTENERS: INSIDE://0.0.0.0:19092,OUTSIDE://0.0.0.0:9092
+ KAFKA_INTER_BROKER_LISTENER_NAME: INSIDE
+ KAFKA_LOG_DIRS: /kafka/kafka-logs
+ KAFKA_DELETE_TOPIC_ENABLE: "true"
+ KAFKA_DEFAULT_REPLICATION_FACTOR: 1
+ KAFKA_NUM_PARTITIONS: 3
+ links:
+ - cw-zookeeper
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ddc2169..35f851c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,23 +19,49 @@
org.springframework.boot
- spring-boot-starter-data-jdbc
+ spring-boot-starter-jdbc
+
org.springframework.boot
spring-boot-starter-web
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.16
+ provided
+
+
org.postgresql
postgresql
runtime
+
org.springframework.boot
spring-boot-starter-test
test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+ org.mockito
+ mockito-junit-jupiter
+ test
+
diff --git a/postman-collection/crypto.postman_collection.json b/postman-collection/crypto.postman_collection.json
new file mode 100644
index 0000000..c0ef6c7
--- /dev/null
+++ b/postman-collection/crypto.postman_collection.json
@@ -0,0 +1,281 @@
+{
+ "info": {
+ "_postman_id": "304b1991-0121-4aed-a874-b29e1735c4a3",
+ "name": "crypto",
+ "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
+ },
+ "item": [
+ {
+ "name": "healthcheck",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": null
+ },
+ "response": []
+ },
+ {
+ "name": "get user by id",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/users/6",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "users",
+ "6"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "get users",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/users",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "users"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "create user",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\": \"pepe\",\n \"password\": \"pass1234\",\n \"email\": \"pepe@gmail.com\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/users",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "users"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "update user",
+ "request": {
+ "method": "PUT",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"username\":\"pepito\",\n \"password\":\"soypepito\",\n \"email\":\"pepito@pepito\"\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/users/2",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "users",
+ "2"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "delete user",
+ "request": {
+ "method": "DELETE",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/users/15",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "users",
+ "15"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "get coins quotes",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/cryptocurrencies/quotes",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "cryptocurrencies",
+ "quotes"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "get portfolio",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/portfolios/10",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "portfolios",
+ "10"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "get transaction history",
+ "request": {
+ "method": "GET",
+ "header": [],
+ "url": {
+ "raw": "http://localhost:8080/transactions/8",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "transactions",
+ "8"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "transfer",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"issuer\": 88,\n \"receiver\": 2,\n \"cryptocurrency\": \"Huobi Token\",\n \"amount\": 6000\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/transactions/transferences",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "transactions",
+ "transferences"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "buys",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"userId\": 3,\n \"cryptocurrency\": \"Bitcoin\",\n \"amountInUsd\": 100000\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/transactions/buys",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "transactions",
+ "buys"
+ ]
+ }
+ },
+ "response": []
+ },
+ {
+ "name": "sells",
+ "request": {
+ "method": "POST",
+ "header": [],
+ "body": {
+ "mode": "raw",
+ "raw": "{\n \"userId\": 3,\n \"cryptocurrency\": \"Bitcoin\",\n \"amount\": 1\n}",
+ "options": {
+ "raw": {
+ "language": "json"
+ }
+ }
+ },
+ "url": {
+ "raw": "http://localhost:8080/transactions/sells",
+ "protocol": "http",
+ "host": [
+ "localhost"
+ ],
+ "port": "8080",
+ "path": [
+ "transactions",
+ "sells"
+ ]
+ }
+ },
+ "response": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/sql/create-database.sql b/sql/create-database.sql
new file mode 100644
index 0000000..b017bd2
--- /dev/null
+++ b/sql/create-database.sql
@@ -0,0 +1,306 @@
+DROP TABLE IF EXISTS "users";
+
+CREATE TABLE IF NOT EXISTS "users" (
+ id BIGSERIAL PRIMARY KEY,
+ username VARCHAR(255) NOT NULL,
+ password VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ enabled BOOLEAN DEFAULT TRUE
+);
+
+DROP TABLE IF EXISTS "cryptocurrency";
+
+CREATE TABLE IF NOT EXISTS "cryptocurrency" (
+ id BIGSERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ symbol VARCHAR(255) NOT NULL
+);
+
+DROP TABLE IF EXISTS "transaction";
+
+CREATE TABLE IF NOT EXISTS "transaction" (
+ id BIGSERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ crypto_currency_id INTEGER NOT NULL,
+ amount NUMERIC(22,0) NOT NULL,
+ operation_type VARCHAR(50) NOT NULL,
+ transaction_date VARCHAR(255) NOT NULL,
+ CONSTRAINT fk_user_id FOREIGN KEY(user_id) REFERENCES "users"(id),
+ CONSTRAINT fk_cryptocurrency FOREIGN KEY(crypto_currency_id) REFERENCES cryptocurrency(id)
+);
+
+INSERT INTO "cryptocurrency" ("id", "name", "symbol") VALUES
+(1, 'Bitcoin', 'BTC'),
+(2, 'Ethereum', 'ETH'),
+(3, 'Tether', 'USDT'),
+(4, 'XRP', 'XRP'),
+(5, 'Polkadot', 'DOT'),
+(6, 'Cardano', 'ADA'),
+(7, 'Chainlink', 'LINK'),
+(8, 'Litecoin', 'LTC'),
+(9, 'Binance Coin', 'BNB'),
+(10, 'Bitcoin Cash', 'BCH'),
+(11, 'Stellar', 'XLM'),
+(12, 'Dogecoin', 'DOGE'),
+(13, 'USD Coin', 'USDC'),
+(14, 'Aave', 'AAVE'),
+(15, 'Uniswap', 'UNI'),
+(16, 'Wrapped Bitcoin', 'WBTC'),
+(17, 'Bitcoin SV', 'BSV'),
+(18, 'EOS', 'EOS'),
+(19, 'Monero', 'XMR'),
+(20, 'NEM', 'XEM'),
+(21, 'TRON', 'TRX'),
+(22, 'Synthetix', 'SNX'),
+(23, 'Tezos', 'XTZ'),
+(24, 'Maker', 'MKR'),
+(25, 'Compound', 'COMP'),
+(26, 'THETA', 'THETA'),
+(27, 'SushiSwap', 'SUSHI'),
+(28, 'Cosmos', 'ATOM'),
+(29, 'UMA', 'UMA'),
+(30, 'VeChain', 'VET'),
+(31, 'Dai', 'DAI'),
+(32, 'Solana', 'SOL'),
+(33, 'Neo', 'NEO'),
+(34, 'Huobi Token', 'HT'),
+(35, 'Crypto.com Coin', 'CRO'),
+(36, 'Binance USD', 'BUSD'),
+(37, 'UNUS SED LEO', 'LEO'),
+(38, 'Terra', 'LUNA'),
+(39, 'Elrond', 'EGLD'),
+(40, 'FTX Token', 'FTT'),
+(41, 'IOTA', 'MIOTA'),
+(42, 'Avalanche', 'AVAX'),
+(43, 'Celsius', 'CEL'),
+(44, 'Dash', 'DASH'),
+(45, 'Filecoin', 'FIL'),
+(46, 'The Graph', 'GRT'),
+(47, 'Zcash', 'ZEC'),
+(48, 'Kusama', 'KSM'),
+(49, 'Revain', 'REV'),
+(50, 'Decred', 'DCR'),
+(51, 'yearn.finance', 'YFI'),
+(52, 'Algorand', 'ALGO'),
+(53, 'Ethereum Classic', 'ETC'),
+(54, 'Ren', 'REN'),
+(55, 'Zilliqa', 'ZIL'),
+(56, 'SwissBorg', 'CHSB'),
+(57, 'Waves', 'WAVES'),
+(58, '0x', 'ZRX'),
+(59, 'Nexo', 'NEXO'),
+(60, 'NEAR Protocol', 'NEAR'),
+(61, 'Curve DAO Token', 'CRV'),
+(62, 'Loopring', 'LRC'),
+(63, 'OMG Network', 'OMG'),
+(64, 'Hedera Hashgraph', 'HBAR'),
+(65, 'THORChain', 'RUNE'),
+(66, 'renBTC', 'RENBTC'),
+(67, '1inch', '1INCH'),
+(68, 'Voyager Token', 'VGX'),
+(69, 'Celo', 'CELO'),
+(70, 'Ontology', 'ONT'),
+(71, 'HedgeTrade', 'HEDG'),
+(72, 'Basic Attention Token', 'BAT'),
+(73, 'HUSD', 'HUSD'),
+(74, 'Nano', 'NANO'),
+(75, 'ICON', 'ICX'),
+(76, 'DigiByte', 'DGB'),
+(77, 'Quant', 'QNT'),
+(78, 'BitTorrent', 'BTT'),
+(79, 'Alpha Finance Lab', 'ALPHA'),
+(80, 'Siacoin', 'SC'),
+(81, 'Horizen', 'ZEN'),
+(82, 'Reserve Rights', 'RSR'),
+(83, 'TrueUSD', 'TUSD'),
+(84, 'OKB', 'OKB'),
+(85, 'Ampleforth', 'AMPL'),
+(86, 'Fantom', 'FTM'),
+(87, 'Qtum', 'QTUM'),
+(88, 'Stacks', 'STX'),
+(89, 'Kyber Network', 'KNC'),
+(90, 'Enjin Coin', 'ENJ'),
+(91, 'Ocean Protocol', 'OCEAN'),
+(92, 'PancakeSwap', 'CAKE'),
+(93, 'FunFair', 'FUN'),
+(94, 'Verge', 'XVG'),
+(95, 'Bancor', 'BNT'),
+(96, 'IOST', 'IOST'),
+(97, 'Decentraland', 'MANA'),
+(98, 'TerraUSD', 'UST'),
+(99, 'Bitcoin BEP2', 'BTCB'),
+(100, 'Paxos Standard', 'PAX');
+
+INSERT INTO "users" ("username", "password", "email") VALUES
+('freeman.mohr', '0l6lyyy8x', 'Romana.Pagac@gmail.com'),
+('tobias.shields', 's26i95xz49e4ko', 'Lasandra.Ward@gmail.com'),
+('houston.luettgen', 'lkf9ttsxcy', 'Virgilio.Miller@gmail.com'),
+('gladis.trantow', 'fcq9s1rmg', 'Colby.Wiza@gmail.com'),
+('anita.flatley', '3y23lwum', 'Clinton.Douglas@gmail.com'),
+('belva.nitzsche', 'xrqgmnocosaqm', 'Reynalda.Simonis@gmail.com'),
+('modesto.tromp', 'bw8n3cf9', 'Cristobal.Kub@gmail.com'),
+('delcie.beatty', 'gb3uyxo4pmk9', 'Renato.Gerlach@gmail.com'),
+('harris.heller', 'szzrv2defr', 'Hayden.Goyette@gmail.com'),
+('victor.block', '1ywk3oqq', 'Logan.Toy@gmail.com'),
+('bobbi.grady', '8yas0ov2scd', 'Laurence.Rosenbaum@gmail.com'),
+('rosanna.leffler', '9bvm81mkg', 'Ellsworth.Robel@gmail.com'),
+('claud.cole', 'pnysduri10r5', 'Versie.Morar@gmail.com'),
+('wilbert.spencer', '1tcw01j6m', 'Curt.Leannon@gmail.com'),
+('jody.daniel', 'avb30s61lwkg', 'Brice.Bergnaum@gmail.com'),
+('marg.wiza', 'uas1ap2r4xl5', 'Clair.Welch@gmail.com'),
+('kaylene.witting', 'bkgxa04sr7ed', 'Soon.Cole@gmail.com'),
+('merlene.weimann', 'fd4e1lekm9xiw2', 'Colton.Rogahn@gmail.com'),
+('russel.quitzon', 'logsrv9xpp', 'Daina.Kuhlman@gmail.com'),
+('demetrius.greenfelder', 'eczqsqh4budxle', 'Michaela.Corkery@gmail.com'),
+('denny.bayer', 'arhcaqjd7', 'Cheryll.Borer@gmail.com'),
+('gearldine.robel', 'tds399v7n', 'Lanora.Runte@gmail.com'),
+('richard.leannon', '2cfiw7zt2v', 'Josiah.O''Connell@gmail.com'),
+('dorsey.satterfield', 'mfxzhp3231q3z2c', 'Marceline.Nitzsche@gmail.com'),
+('elisabeth.halvorson', 'tdpk0jxbz2st', 'Effie.Harber@gmail.com'),
+('fallon.turner', '55d5z8cv', 'Miles.Konopelski@gmail.com'),
+('clemente.krajcik', 'ysie202bxm9owu0', 'Oren.Nikolaus@gmail.com'),
+('jean.mraz', 'fjqmql4s', 'Romelia.Zieme@gmail.com'),
+('hai.klein', 'uil3l3bng', 'Adeline.Halvorson@gmail.com'),
+('yvone.kuhlman', 'odwkq329wmpq', 'Christeen.Kirlin@gmail.com'),
+('alex.haag', 'hueh2j2yn5', 'Donnell.Zboncak@gmail.com'),
+('krista.hessel', 'f3dcof99hbc', 'Garnet.Gottlieb@gmail.com'),
+('alethia.schneider', '2ugs3t0rc', 'Darcey.Becker@gmail.com'),
+('ferdinand.bauch', 'puy7x7kz3t', 'Crista.Moen@gmail.com'),
+('trey.abernathy', 'jpom4f25x19', 'Ben.Cremin@gmail.com'),
+('rickey.kilback', '8sdw7o4cor9obkl', 'Shena.Morissette@gmail.com'),
+('carlee.rippin', 'mvw1o9sfmhont2', 'Candy.Weimann@gmail.com'),
+('eddy.leffler', 'cchggeszk6w0', 'Wilhemina.Marquardt@gmail.com'),
+('tanisha.jacobson', 'd9tllti8m5jqo', 'Malorie.Rippin@gmail.com'),
+('efren.schaden', 'im958e1y', 'Isiah.Hoppe@gmail.com'),
+('clint.bogan', 'ut7smk82nfhld', 'Artie.Fritsch@gmail.com'),
+('kim.herzog', 'eaj84aw85ui', 'Katharina.Mraz@gmail.com'),
+('virgilio.ondricka', 'v3jnx770rmaskv', 'Tinisha.Deckow@gmail.com'),
+('debroah.rath', 'di5zibt6eeono', 'Star.Kohler@gmail.com'),
+('meg.barrows', 'u5nd2gzmwzn', 'Donald.Ondricka@gmail.com'),
+('bree.terry', 'ycpaiu620qg08', 'Jacques.Bogisich@gmail.com'),
+('lesli.kertzmann', 'q5scoutvf04m3g9', 'Antione.Waters@gmail.com'),
+('harris.waters', 'vrf4hclmw', 'Rosendo.Jacobi@gmail.com'),
+('magan.witting', 'wraf00bxagtm662', 'Andree.Mayer@gmail.com'),
+('edison.mraz', 'ox2vmyeufxj8', 'Ngan.Walker@gmail.com'),
+('joaquin.denesik', 'jfa3hs3qy', 'Meridith.Gleason@gmail.com'),
+('jacki.littel', 'rn3ryupxisuavg', 'Leoma.Keeling@gmail.com'),
+('grayce.armstrong', 'ibqalza3psvs', 'Genaro.Doyle@gmail.com'),
+('carter.hills', '4sq3ox409oal', 'Sabrina.Kling@gmail.com'),
+('reggie.hirthe', '2j0bxs1hvzvg1wm', 'Holley.Boyle@gmail.com'),
+('bernard.stroman', '5328isd173g0c95', 'Val.Abbott@gmail.com'),
+('monique.mertz', 'tg7953mtq', 'Herbert.Wintheiser@gmail.com'),
+('corey.cummerata', 'iebttzxj', 'Julene.Boyle@gmail.com'),
+('lorinda.windler', 'yahvq9aq', 'Bernard.Rau@gmail.com'),
+('alejandra.hilll', 'zb79kczroxm51c', 'Lula.Boyle@gmail.com'),
+('jamika.west', 'jekhea3p1ha', 'Jarred.Kunze@gmail.com'),
+('isidro.mraz', 'k46r9kenclep77', 'Jimmy.Abbott@gmail.com'),
+('britt.rempel', 'p6qw6138', 'Stacia.Wyman@gmail.com'),
+('jocelyn.grimes', 'x176pkj83j', 'Sherrie.Lesch@gmail.com'),
+('stanton.raynor', '0bwai8ak2jbb', 'Jung.Walsh@gmail.com'),
+('howard.hoeger', '91lzg21178rrns', 'Dara.Schimmel@gmail.com'),
+('tressa.willms', 'ynlpv87o4u', 'Blanch.Gislason@gmail.com'),
+('jenice.jacobson', '7n7obytvq', 'Georgeanna.Ondricka@gmail.com'),
+('noe.yundt', 'c6cogh07ao02', 'Salina.Heathcote@gmail.com'),
+('clark.becker', '7kl454rqn', 'Gregg.Sporer@gmail.com'),
+('bridgett.hegmann', 'i4ejb7jiv', 'Maribel.Lesch@gmail.com'),
+('marica.block', 'y70l8isaj', 'Dominque.Ankunding@gmail.com'),
+('jerrell.hudson', 'intfxyg0tqtklnm', 'Rigoberto.Lesch@gmail.com'),
+('kiara.koss', 'ra5y4419k', 'Marlin.Abbott@gmail.com'),
+('verdell.keeling', '5w235265r8g6ps', 'Jeromy.Heller@gmail.com'),
+('modesto.heller', '5xywex1ctuota9', 'Lexie.Lakin@gmail.com'),
+('gina.larkin', '9qzvugg7k', 'Rena.Lubowitz@gmail.com'),
+('avelina.damore', 'wnjgydgaazwb0n', 'Lucretia.Boehm@gmail.com'),
+('karena.buckridge', 'qipe23tvcs', 'Willis.Abshire@gmail.com'),
+('celina.hermiston', 'adma7spn', 'Kendall.Dooley@gmail.com'),
+('shirlene.dare', 'xdysxj7x', 'Babette.Kilback@gmail.com'),
+('juan.hoppe', 'ygj3sidx', 'Javier.Kuphal@gmail.com'),
+('phillip.weimann', '7rktwi7xc', 'Lea.Wisozk@gmail.com'),
+('teri.dach', 'd68vaom3f0ni3', 'Aron.White@gmail.com'),
+('marcelo.sawayn', '3hrkcf6ubidwk5', 'Thaddeus.Farrell@gmail.com'),
+('lottie.kautzer', 'm1blbmt9h6wuo6', 'Margarete.Kuphal@gmail.com'),
+('tamara.boehm', 'dsnpnc5z', 'Asley.Kertzmann@gmail.com'),
+('genaro.batz', 'nsdb1drpk', 'Andre.Champlin@gmail.com'),
+('lauren.gerlach', 'fm2izq9cajqq', 'Lonnie.Bogan@gmail.com'),
+('valentine.walter', 'wap7lv3ih3izqcl', 'Lakia.Graham@gmail.com'),
+('gregory.harvey', 'magvzjbtlkuuw', 'Wiley.Beahan@gmail.com'),
+('adalberto.pfannerstill', 'mqcl56y86k139', 'Roberta.Price@gmail.com'),
+('ross.mohr', 'gmhisy9v7jj5', 'Jeanmarie.Grimes@gmail.com'),
+('abby.mcclure', 'gvi8t20q7e', 'Annalee.McLaughlin@gmail.com'),
+('jonna.oconner', '8tao7j9h9i30ng', 'Alison.Sanford@gmail.com'),
+('dannie.larson', 'rap6k0sbwjl', 'Sydney.Schumm@gmail.com'),
+('muriel.russel', 'qkjkqhjl8', 'Cyril.Sawayn@gmail.com'),
+('harlan.larson', 'mlt6jmns0', 'Winona.Brown@gmail.com'),
+('analisa.franecki', '0snjt0kjg0r5k', 'Latrina.Carter@gmail.com'),
+('eric.dietrich', 'mkrwc894nn9ir2', 'Hershel.Adams@gmail.com');
+
+INSERT INTO "transaction" ("user_id", "crypto_currency_id", "amount", "operation_type", "transaction_date") VALUES
+(40, 25, 207, 'BUY', '2021-02-05T19:26:30.091'),
+(85, 31, 334, 'BUY', '2021-02-05T19:26:30.101'),
+(92, 57, 9642, 'BUY', '2021-02-05T19:26:30.105'),
+(98, 1, 2606, 'BUY', '2021-02-05T19:28:43.079'),
+(69, 39, 8215, 'BUY', '2021-02-05T19:28:43.082'),
+(96, 61, 8535, 'BUY', '2021-02-05T19:28:43.086'),
+(50, 3, 2559, 'BUY', '2021-02-05T19:28:43.089'),
+(13, 42, 193, 'BUY', '2021-02-05T19:28:43.095'),
+(20, 46, 3805, 'BUY', '2021-02-05T19:28:43.098'),
+(4, 81, 5778, 'BUY', '2021-02-05T19:28:43.104'),
+(88, 90, 6471, 'BUY', '2021-02-05T19:28:43.111'),
+(84, 13, 1645, 'BUY', '2021-02-05T19:28:43.117'),
+(46, 89, 3558, 'BUY', '2021-02-05T19:28:43.128'),
+(99, 45, 8986, 'BUY', '2021-02-05T19:28:43.131'),
+(88, 69, 5435, 'BUY', '2021-02-05T19:28:43.134'),
+(72, 3, 8340, 'BUY', '2021-02-05T19:28:43.137'),
+(86, 45, 8465, 'BUY', '2021-02-05T19:28:43.141'),
+(24, 96, 6725, 'BUY', '2021-02-05T19:28:43.149'),
+(43, 54, 689, 'BUY', '2021-02-05T19:28:43.152'),
+(78, 50, 2110, 'BUY', '2021-02-05T19:28:43.156'),
+(19, 24, 7530, 'BUY', '2021-02-05T19:28:43.160'),
+(53, 98, 3972, 'BUY', '2021-02-05T19:28:43.168'),
+(51, 70, 5646, 'BUY', '2021-02-05T19:29:03.203'),
+(97, 13, 9228, 'BUY', '2021-02-05T19:29:03.207'),
+(40, 31, 8321, 'BUY', '2021-02-05T19:29:03.218'),
+(50, 93, 3650, 'BUY', '2021-02-05T19:29:03.223'),
+(60, 18, 7363, 'BUY', '2021-02-05T19:29:03.231'),
+(21, 49, 8854, 'BUY', '2021-02-05T19:29:03.242'),
+(70, 5, 7481, 'BUY', '2021-02-05T19:29:03.256'),
+(43, 37, 3791, 'BUY', '2021-02-05T19:29:03.263'),
+(99, 5, 5487, 'BUY', '2021-02-05T19:29:03.271'),
+(38, 65, 5749, 'BUY', '2021-02-05T19:29:03.288'),
+(20, 95, 4628, 'BUY', '2021-02-05T19:29:03.294'),
+(88, 34, 5985, 'BUY', '2021-02-05T19:29:08.804'),
+(89, 16, 8370, 'BUY', '2021-02-05T19:29:08.825'),
+(50, 91, -353, 'SELL', '2021-02-05T19:28:43.068'),
+(65, 23, -6060, 'SELL', '2021-02-05T19:28:43.163'),
+(68, 91, -6453, 'SELL', '2021-02-05T19:29:03.190'),
+(37, 70, -4943, 'SELL', '2021-02-05T19:29:03.199'),
+(8, 50, -2637, 'SELL', '2021-02-05T19:29:03.211'),
+(75, 2, -2891, 'SELL', '2021-02-05T19:29:03.249'),
+(94, 77, -481, 'SELL', '2021-02-05T19:29:03.253'),
+(88, 72, -6248, 'SELL', '2021-02-05T19:29:03.267'),
+(81, 42, -7979, 'SELL', '2021-02-05T19:29:08.814'),
+(95, 65, -4064, 'SELL', '2021-02-05T19:29:08.821'),
+(92, 73, -2394, 'SELL', '2021-02-05T19:29:08.828'),
+(95, 74, -5233, 'TRANSFERENCE', '2021-02-05T19:26:30.079'),
+(78, 45, -6713, 'TRANSFERENCE', '2021-02-05T19:26:30.088'),
+(16, 94, -4969, 'TRANSFERENCE', '2021-02-05T19:26:30.097'),
+(61, 47, -9508, 'TRANSFERENCE', '2021-02-05T19:28:43.092'),
+(66, 3, -5913, 'TRANSFERENCE', '2021-02-05T19:28:43.101'),
+(93, 96, -1302, 'TRANSFERENCE', '2021-02-05T19:28:43.107'),
+(14, 22, -6734, 'TRANSFERENCE', '2021-02-05T19:28:43.114'),
+(48, 98, -5015, 'TRANSFERENCE', '2021-02-05T19:28:43.121'),
+(1, 66, -2212, 'TRANSFERENCE', '2021-02-05T19:28:43.125'),
+(51, 77, -2776, 'TRANSFERENCE', '2021-02-05T19:28:43.145'),
+(77, 62, -6933, 'TRANSFERENCE', '2021-02-05T19:28:43.165'),
+(64, 55, -4621, 'TRANSFERENCE', '2021-02-05T19:29:03.215'),
+(42, 27, -2335, 'TRANSFERENCE', '2021-02-05T19:29:03.226'),
+(24, 8, -2545, 'TRANSFERENCE', '2021-02-05T19:29:03.235'),
+(10, 46, -3531, 'TRANSFERENCE', '2021-02-05T19:29:03.239'),
+(68, 42, -559, 'TRANSFERENCE', '2021-02-05T19:29:03.245'),
+(35, 53, -8838, 'TRANSFERENCE', '2021-02-05T19:29:03.260'),
+(56, 7, -16, 'TRANSFERENCE', '2021-02-05T19:29:03.275'),
+(76, 33, -423, 'TRANSFERENCE', '2021-02-05T19:29:03.279'),
+(48, 87, -4904, 'TRANSFERENCE', '2021-02-05T19:29:03.283'),
+(90, 51, -5776, 'TRANSFERENCE', '2021-02-05T19:29:03.291'),
+(56, 9, -1794, 'TRANSFERENCE', '2021-02-05T19:29:08.811'),
+(59, 65, -973, 'TRANSFERENCE', '2021-02-05T19:29:08.818'),
+(95, 13, -8877, 'TRANSFERENCE', '2021-02-05T19:29:08.831');
\ No newline at end of file
diff --git a/src/main/java/com/serdeliverance/cryptowallet/api/CryptocurrencyController.java b/src/main/java/com/serdeliverance/cryptowallet/api/CryptocurrencyController.java
new file mode 100644
index 0000000..55374a8
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/api/CryptocurrencyController.java
@@ -0,0 +1,26 @@
+package com.serdeliverance.cryptowallet.api;
+
+import com.serdeliverance.cryptowallet.dto.CurrencyQuoteDTO;
+import com.serdeliverance.cryptowallet.services.CryptocurrencyService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/cryptocurrencies")
+@RequiredArgsConstructor
+@Slf4j
+public class CryptocurrencyController {
+
+ private final CryptocurrencyService cryptocurrencyService;
+
+ @GetMapping("/quotes")
+ public List quotes() {
+ log.info("Getting quotes");
+ return cryptocurrencyService.quotes();
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/api/HealthCheckController.java b/src/main/java/com/serdeliverance/cryptowallet/api/HealthCheckController.java
new file mode 100644
index 0000000..1d3cea3
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/api/HealthCheckController.java
@@ -0,0 +1,18 @@
+package com.serdeliverance.cryptowallet.api;
+
+import com.serdeliverance.cryptowallet.dto.StatusDTO;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static com.serdeliverance.cryptowallet.dto.StatusDTO.ok;
+
+@RestController
+@RequestMapping("/healthcheck")
+public class HealthCheckController {
+
+ @GetMapping
+ public StatusDTO healthcheck() {
+ return ok();
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/api/PortfolioController.java b/src/main/java/com/serdeliverance/cryptowallet/api/PortfolioController.java
new file mode 100644
index 0000000..cf96060
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/api/PortfolioController.java
@@ -0,0 +1,25 @@
+package com.serdeliverance.cryptowallet.api;
+
+import com.serdeliverance.cryptowallet.dto.PorfolioDTO;
+import com.serdeliverance.cryptowallet.services.PortfolioService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/portfolios")
+@RequiredArgsConstructor
+@Slf4j
+public class PortfolioController {
+
+ private final PortfolioService portfolioService;
+
+ @GetMapping("/{userId}")
+ public PorfolioDTO getPortfolio(@PathVariable("userId") Integer userId) {
+ log.info("Getting crypto portfolio for user: {}", userId);
+ return portfolioService.getPortfolio(userId);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/api/TransactionController.java b/src/main/java/com/serdeliverance/cryptowallet/api/TransactionController.java
new file mode 100644
index 0000000..59ee11f
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/api/TransactionController.java
@@ -0,0 +1,52 @@
+package com.serdeliverance.cryptowallet.api;
+
+import com.serdeliverance.cryptowallet.dto.BuyDTO;
+import com.serdeliverance.cryptowallet.dto.SellDTO;
+import com.serdeliverance.cryptowallet.dto.TransactionDTO;
+import com.serdeliverance.cryptowallet.dto.TransferenceDTO;
+import com.serdeliverance.cryptowallet.services.TransactionService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.List;
+
+@RestController
+@RequestMapping("/transactions")
+@RequiredArgsConstructor
+@Slf4j
+public class TransactionController {
+
+ private final TransactionService transactionService;
+
+ @GetMapping("/{userId}")
+ public List getHistory(@PathVariable("userId") Integer userId) {
+ log.info("Getting transaction history for userId: {}", userId);
+ return transactionService.getHistory(userId);
+ }
+
+ @PostMapping("/transferences")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void transfer(@RequestBody TransferenceDTO transferenceDTO) {
+ log.info("Performing transference from issuer {} to receiver {}",
+ transferenceDTO.getIssuer(), transferenceDTO.getReceiver());
+ transactionService.transfer(transferenceDTO.getIssuer(), transferenceDTO.getReceiver(),
+ transferenceDTO.getCryptocurrency(), transferenceDTO.getAmount());
+ }
+
+ @PostMapping("/buys")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void buy(@Valid @RequestBody BuyDTO buyDTO) {
+ log.info("Perfoming buy by user {}", buyDTO.getUserId());
+ transactionService.buy(buyDTO.getUserId(), buyDTO.getCryptocurrency(), buyDTO.getAmountInUsd());
+ }
+
+ @PostMapping("/sells")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void buy(@Valid @RequestBody SellDTO sellDTO) {
+ log.info("Perfoming selling by user {}", sellDTO.getUserId());
+ transactionService.sell(sellDTO.getUserId(), sellDTO.getCryptocurrency(), sellDTO.getAmount());
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/api/UserController.java b/src/main/java/com/serdeliverance/cryptowallet/api/UserController.java
new file mode 100644
index 0000000..6767cce
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/api/UserController.java
@@ -0,0 +1,65 @@
+package com.serdeliverance.cryptowallet.api;
+
+import com.serdeliverance.cryptowallet.converters.UserDTOConverter;
+import com.serdeliverance.cryptowallet.dto.CreateUserDTO;
+import com.serdeliverance.cryptowallet.dto.UpdateUserDTO;
+import com.serdeliverance.cryptowallet.dto.UserDTO;
+import com.serdeliverance.cryptowallet.exceptions.ResourceNotFoundException;
+import com.serdeliverance.cryptowallet.services.UserService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.serdeliverance.cryptowallet.converters.UserDTOConverter.convertToModel;
+
+@RestController
+@RequestMapping("/users")
+@RequiredArgsConstructor
+@Slf4j
+public class UserController {
+
+ private final UserService userService;
+
+ @GetMapping("/{id}")
+ public UserDTO get(@PathVariable("id") Integer id) {
+ log.info("Getting user with id: {}", id);
+ return userService
+ .get(id)
+ .map(UserDTOConverter::convertToDTO)
+ .orElseThrow(() -> new ResourceNotFoundException("user:" + id));
+ }
+
+ @GetMapping
+ public List getAll() {
+ log.info("Getting all users");
+ return userService
+ .getAll()
+ .stream()
+ .map(UserDTOConverter::convertToDTO)
+ .collect(Collectors.toList());
+ }
+
+ @PostMapping
+ @ResponseStatus(HttpStatus.CREATED)
+ public void create(@RequestBody CreateUserDTO user) {
+ log.info("creating user...");
+ userService.create(convertToModel(user));
+ }
+
+ @PutMapping("/{id}")
+ public void update(@PathVariable("id") Integer id, @RequestBody UpdateUserDTO updateUser) {
+ log.info("updating user: {}", id);
+ userService.update(convertToModel(id, updateUser));
+ }
+
+ @DeleteMapping("/{id}")
+ @ResponseStatus(HttpStatus.NO_CONTENT)
+ public void delete(@PathVariable("id") Integer id) {
+ log.info("deleting user with userId: {}", id);
+ userService.delete(id);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/api/exception/ApiExceptionHandler.java b/src/main/java/com/serdeliverance/cryptowallet/api/exception/ApiExceptionHandler.java
new file mode 100644
index 0000000..b8d494f
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/api/exception/ApiExceptionHandler.java
@@ -0,0 +1,42 @@
+package com.serdeliverance.cryptowallet.api.exception;
+
+import com.serdeliverance.cryptowallet.exceptions.InvalidOperationException;
+import com.serdeliverance.cryptowallet.exceptions.RemoteApiException;
+import com.serdeliverance.cryptowallet.exceptions.ResourceNotFoundException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+
+@ControllerAdvice
+@Slf4j
+public class ApiExceptionHandler {
+
+ @ExceptionHandler(value = {RuntimeException.class, RemoteApiException.class})
+ public ResponseEntity internalServerError(
+ RuntimeException runtimeException
+ ) {
+ log.error("Unexpected error. Error {}", runtimeException.getMessage());
+ return ResponseEntity
+ .status(HttpStatus.INTERNAL_SERVER_ERROR).build();
+ }
+
+ @ExceptionHandler(value = {ResourceNotFoundException.class})
+ public ResponseEntity notFoundError(
+ ResourceNotFoundException resourceNotFoundException
+ ) {
+ log.info("Resource not found. {}", resourceNotFoundException.getMessage());
+ return ResponseEntity
+ .status(HttpStatus.NOT_FOUND).build();
+ }
+
+ @ExceptionHandler(value = {InvalidOperationException.class})
+ public ResponseEntity invalidOperation(
+ InvalidOperationException invalidOperationException
+ ) {
+ log.info("Invalid operation. {}", invalidOperationException.getMessage());
+ return ResponseEntity
+ .status(HttpStatus.BAD_REQUEST).build();
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/clients/CoinmarketCapClient.java b/src/main/java/com/serdeliverance/cryptowallet/clients/CoinmarketCapClient.java
new file mode 100644
index 0000000..8fe86de
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/clients/CoinmarketCapClient.java
@@ -0,0 +1,47 @@
+package com.serdeliverance.cryptowallet.clients;
+
+import com.serdeliverance.cryptowallet.clients.response.ListingQuotesResponseDTO;
+import com.serdeliverance.cryptowallet.exceptions.RemoteApiException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Component;
+import org.springframework.web.client.RestTemplate;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class CoinmarketCapClient {
+
+ private static final String API_KEY_HEADER = "X-CMC_PRO_API_KEY";
+
+ private final RestTemplate restTemplate;
+
+ @Value("${coinmarketcap.api-key}")
+ private String apiKey;
+
+ @Value("${coinmarketcap.url}")
+ private String url;
+
+ public ListingQuotesResponseDTO quotes() {
+ log.info("Getting quotes from coinmarketcap");
+ ResponseEntity response = restTemplate.exchange(url, HttpMethod.GET, createEntityWithHeader(API_KEY_HEADER, apiKey), ListingQuotesResponseDTO.class);
+ return handleResponse(response);
+ }
+
+ private ListingQuotesResponseDTO handleResponse(ResponseEntity response) {
+ if (response.getStatusCode() != HttpStatus.OK) {
+ log.error("Coinmarketcap responded with {} status code", response.getStatusCodeValue());
+ throw new RemoteApiException("error communicating with coinmarketcap API");
+ }
+ return response.getBody();
+ }
+
+ private HttpEntity createEntityWithHeader(String header, String value) {
+ HttpHeaders headers = new HttpHeaders();
+ headers.add(header, value);
+ HttpEntity entity = new HttpEntity<>(null, headers);
+ return entity;
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/clients/response/ListingElementDTO.java b/src/main/java/com/serdeliverance/cryptowallet/clients/response/ListingElementDTO.java
new file mode 100644
index 0000000..861756f
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/clients/response/ListingElementDTO.java
@@ -0,0 +1,16 @@
+package com.serdeliverance.cryptowallet.clients.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ListingElementDTO {
+ private String name;
+ private String symbol;
+ private Map quote;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/clients/response/ListingQuotesResponseDTO.java b/src/main/java/com/serdeliverance/cryptowallet/clients/response/ListingQuotesResponseDTO.java
new file mode 100644
index 0000000..3fabc56
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/clients/response/ListingQuotesResponseDTO.java
@@ -0,0 +1,14 @@
+package com.serdeliverance.cryptowallet.clients.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ListingQuotesResponseDTO {
+ private List data;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/clients/response/QuoteDTO.java b/src/main/java/com/serdeliverance/cryptowallet/clients/response/QuoteDTO.java
new file mode 100644
index 0000000..6deb432
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/clients/response/QuoteDTO.java
@@ -0,0 +1,14 @@
+package com.serdeliverance.cryptowallet.clients.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class QuoteDTO {
+ private BigDecimal price;
+}
\ No newline at end of file
diff --git a/src/main/java/com/serdeliverance/cryptowallet/configurations/AppConfiguration.java b/src/main/java/com/serdeliverance/cryptowallet/configurations/AppConfiguration.java
new file mode 100644
index 0000000..ba2ecff
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/configurations/AppConfiguration.java
@@ -0,0 +1,22 @@
+package com.serdeliverance.cryptowallet.configurations;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.web.client.RestTemplate;
+
+import javax.sql.DataSource;
+
+@Configuration
+public class AppConfiguration {
+
+ @Bean
+ public RestTemplate restTemplate() {
+ return new RestTemplate();
+ }
+
+ @Bean
+ public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource ds) {
+ return new NamedParameterJdbcTemplate(ds);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/converters/CurrencyQuoteDTOConverter.java b/src/main/java/com/serdeliverance/cryptowallet/converters/CurrencyQuoteDTOConverter.java
new file mode 100644
index 0000000..92493a6
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/converters/CurrencyQuoteDTOConverter.java
@@ -0,0 +1,19 @@
+package com.serdeliverance.cryptowallet.converters;
+
+import com.serdeliverance.cryptowallet.clients.response.ListingQuotesResponseDTO;
+import com.serdeliverance.cryptowallet.dto.CurrencyQuoteDTO;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+public class CurrencyQuoteDTOConverter {
+
+ private static final String USD_CURRENCY = "USD";
+
+ public static List convertFromResponse(ListingQuotesResponseDTO response) {
+ return response.getData()
+ .stream()
+ .map(elem -> new CurrencyQuoteDTO(elem.getName(), elem.getQuote().get(USD_CURRENCY).getPrice()))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/converters/UserDTOConverter.java b/src/main/java/com/serdeliverance/cryptowallet/converters/UserDTOConverter.java
new file mode 100644
index 0000000..205eba1
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/converters/UserDTOConverter.java
@@ -0,0 +1,39 @@
+package com.serdeliverance.cryptowallet.converters;
+
+import com.serdeliverance.cryptowallet.domain.User;
+import com.serdeliverance.cryptowallet.dto.CreateUserDTO;
+import com.serdeliverance.cryptowallet.dto.UpdateUserDTO;
+import com.serdeliverance.cryptowallet.dto.UserDTO;
+
+import java.util.Optional;
+
+/**
+ * Converter for UserDTO to User and viceversa
+ */
+public class UserDTOConverter {
+
+ public static UserDTO convertToDTO(User user) {
+ return new UserDTO(
+ user.getId(),
+ user.getUsername(),
+ user.getEmail()
+ );
+ }
+
+ public static User convertToModel(CreateUserDTO createUserDTO) {
+ return new User(
+ Optional.empty(),
+ createUserDTO.getUsername(),
+ createUserDTO.getPassword(),
+ createUserDTO.getEmail());
+ }
+
+ public static User convertToModel(Integer id, UpdateUserDTO updateUserDTO) {
+ return new User(
+ Optional.of(id),
+ updateUserDTO.getUsername(),
+ updateUserDTO.getPassword(),
+ updateUserDTO.getEmail()
+ );
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/domain/Cryptocurrency.java b/src/main/java/com/serdeliverance/cryptowallet/domain/Cryptocurrency.java
new file mode 100644
index 0000000..9aafe03
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/domain/Cryptocurrency.java
@@ -0,0 +1,14 @@
+package com.serdeliverance.cryptowallet.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Cryptocurrency {
+ private Integer id;
+ private String name;
+ private String symbol;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/domain/CurrencyTotal.java b/src/main/java/com/serdeliverance/cryptowallet/domain/CurrencyTotal.java
new file mode 100644
index 0000000..1996a9e
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/domain/CurrencyTotal.java
@@ -0,0 +1,13 @@
+package com.serdeliverance.cryptowallet.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CurrencyTotal {
+ private Cryptocurrency cryptocurrency;
+ private Integer amount;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/domain/OperationType.java b/src/main/java/com/serdeliverance/cryptowallet/domain/OperationType.java
new file mode 100644
index 0000000..d1b2a46
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/domain/OperationType.java
@@ -0,0 +1,7 @@
+package com.serdeliverance.cryptowallet.domain;
+
+public enum OperationType {
+ BUY,
+ SELL,
+ TRANSFERENCE
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/domain/Portfolio.java b/src/main/java/com/serdeliverance/cryptowallet/domain/Portfolio.java
new file mode 100644
index 0000000..08c1e25
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/domain/Portfolio.java
@@ -0,0 +1,19 @@
+package com.serdeliverance.cryptowallet.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Portfolio {
+ private User user;
+ private List currencies;
+ private BigDecimal totalInUSD;
+ private LocalDateTime date;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/domain/Transaction.java b/src/main/java/com/serdeliverance/cryptowallet/domain/Transaction.java
new file mode 100644
index 0000000..2d61157
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/domain/Transaction.java
@@ -0,0 +1,20 @@
+package com.serdeliverance.cryptowallet.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Transaction {
+
+ private Long id;
+ private Integer userId;
+ private Integer cryptocurrencyId;
+ private BigDecimal amount;
+ private OperationType operationType;
+ private String transactionDate;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/domain/User.java b/src/main/java/com/serdeliverance/cryptowallet/domain/User.java
new file mode 100644
index 0000000..3f9bc5e
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/domain/User.java
@@ -0,0 +1,18 @@
+package com.serdeliverance.cryptowallet.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Optional;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class User {
+
+ private Optional id;
+ private String username;
+ private String password;
+ private String email;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/BuyDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/BuyDTO.java
new file mode 100644
index 0000000..6851442
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/BuyDTO.java
@@ -0,0 +1,20 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.validation.constraints.Min;
+import java.math.BigDecimal;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class BuyDTO {
+
+ private Integer userId;
+ private String cryptocurrency;
+
+ @Min(value = 1, message = "Amount in usd must be valid")
+ private BigDecimal amountInUsd;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/CreateUserDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/CreateUserDTO.java
new file mode 100644
index 0000000..ca742ca
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/CreateUserDTO.java
@@ -0,0 +1,17 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Optional;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateUserDTO {
+ private Optional id;
+ private String username;
+ private String password;
+ private String email;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/CurrencyQuoteDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/CurrencyQuoteDTO.java
new file mode 100644
index 0000000..b351901
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/CurrencyQuoteDTO.java
@@ -0,0 +1,15 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CurrencyQuoteDTO {
+ private String crypto;
+ private BigDecimal quoteInUsd;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/CurrencyTotalDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/CurrencyTotalDTO.java
new file mode 100644
index 0000000..36ae001
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/CurrencyTotalDTO.java
@@ -0,0 +1,15 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class CurrencyTotalDTO {
+ private String currency;
+ private BigDecimal amount;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/PorfolioDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/PorfolioDTO.java
new file mode 100644
index 0000000..dcfe493
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/PorfolioDTO.java
@@ -0,0 +1,25 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class PorfolioDTO {
+
+ private Integer userId;
+ private List currencies;
+ private BigDecimal totalInUSD;
+ private LocalDateTime date;
+
+ public static PorfolioDTO emptyPortfolio(Integer userId, LocalDateTime date) {
+ return new PorfolioDTO(userId, Collections.emptyList(), BigDecimal.ZERO, date);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/SellDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/SellDTO.java
new file mode 100644
index 0000000..8b333a0
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/SellDTO.java
@@ -0,0 +1,16 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class SellDTO {
+ private Integer userId;
+ private String cryptocurrency;
+ private BigDecimal amount;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/StatusDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/StatusDTO.java
new file mode 100644
index 0000000..129a74c
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/StatusDTO.java
@@ -0,0 +1,17 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class StatusDTO {
+
+ private static final String OK_MSG = "ok";
+
+ private String status;
+
+ public static StatusDTO ok() {
+ return new StatusDTO(OK_MSG);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/TransactionDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/TransactionDTO.java
new file mode 100644
index 0000000..75250ae
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/TransactionDTO.java
@@ -0,0 +1,18 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionDTO {
+ private Long id;
+ private String cryptocurrency;
+ private BigDecimal amount;
+ private String operationType;
+ private String date;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/TransferenceDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/TransferenceDTO.java
new file mode 100644
index 0000000..1c933ac
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/TransferenceDTO.java
@@ -0,0 +1,17 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class TransferenceDTO {
+ private Integer issuer;
+ private Integer receiver;
+ private String cryptocurrency;
+ private BigDecimal amount;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/UpdateUserDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/UpdateUserDTO.java
new file mode 100644
index 0000000..22590f4
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/UpdateUserDTO.java
@@ -0,0 +1,17 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Optional;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class UpdateUserDTO {
+ private Optional id;
+ private String username;
+ private String password;
+ private String email;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/dto/UserDTO.java b/src/main/java/com/serdeliverance/cryptowallet/dto/UserDTO.java
new file mode 100644
index 0000000..d13cdb2
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/dto/UserDTO.java
@@ -0,0 +1,16 @@
+package com.serdeliverance.cryptowallet.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Optional;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class UserDTO {
+ private Optional id;
+ private String username;
+ private String email;
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/exceptions/InvalidOperationException.java b/src/main/java/com/serdeliverance/cryptowallet/exceptions/InvalidOperationException.java
new file mode 100644
index 0000000..cb19cab
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/exceptions/InvalidOperationException.java
@@ -0,0 +1,7 @@
+package com.serdeliverance.cryptowallet.exceptions;
+
+public class InvalidOperationException extends RuntimeException {
+ public InvalidOperationException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/exceptions/RemoteApiException.java b/src/main/java/com/serdeliverance/cryptowallet/exceptions/RemoteApiException.java
new file mode 100644
index 0000000..5502420
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/exceptions/RemoteApiException.java
@@ -0,0 +1,8 @@
+package com.serdeliverance.cryptowallet.exceptions;
+
+public class RemoteApiException extends RuntimeException {
+
+ public RemoteApiException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/exceptions/ResourceNotFoundException.java b/src/main/java/com/serdeliverance/cryptowallet/exceptions/ResourceNotFoundException.java
new file mode 100644
index 0000000..c347d92
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/exceptions/ResourceNotFoundException.java
@@ -0,0 +1,8 @@
+package com.serdeliverance.cryptowallet.exceptions;
+
+public class ResourceNotFoundException extends RuntimeException {
+
+ public ResourceNotFoundException(String message) {
+ super(message);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/repositories/CryptocurrencyRepository.java b/src/main/java/com/serdeliverance/cryptowallet/repositories/CryptocurrencyRepository.java
new file mode 100644
index 0000000..a9db9a9
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/repositories/CryptocurrencyRepository.java
@@ -0,0 +1,39 @@
+package com.serdeliverance.cryptowallet.repositories;
+
+import com.serdeliverance.cryptowallet.domain.Cryptocurrency;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.jdbc.core.namedparam.SqlParameterSource;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+@Slf4j
+@RequiredArgsConstructor
+public class CryptocurrencyRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+ private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+
+ private RowMapper mapper =
+ (rs, rowNum) -> new Cryptocurrency(rs.getInt("id"), rs.getString("name"), rs.getString("symbol"));
+
+ public List getByIdList(List ids) {
+ SqlParameterSource parameters = new MapSqlParameterSource("ids", ids);
+ return namedParameterJdbcTemplate
+ .query("SELECT ID, NAME, SYMBOL FROM CRYPTOCURRENCY WHERE ID IN (:ids)",
+ parameters, mapper);
+ }
+
+ public Optional getByName(String name) {
+ return jdbcTemplate.query(
+ "SELECT ID, NAME, SYMBOL FROM CRYPTOCURRENCY WHERE NAME = ?",
+ mapper, name).stream().findFirst();
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/repositories/TransactionRepository.java b/src/main/java/com/serdeliverance/cryptowallet/repositories/TransactionRepository.java
new file mode 100644
index 0000000..6ad5f9b
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/repositories/TransactionRepository.java
@@ -0,0 +1,39 @@
+package com.serdeliverance.cryptowallet.repositories;
+
+import com.serdeliverance.cryptowallet.domain.OperationType;
+import com.serdeliverance.cryptowallet.domain.Transaction;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+
+@Repository
+@Slf4j
+@RequiredArgsConstructor
+public class TransactionRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public List getByUser(Integer userId) {
+
+ return jdbcTemplate.query("SELECT ID, USER_ID, CRYPTO_CURRENCY_ID, AMOUNT, OPERATION_TYPE, TRANSACTION_DATE FROM TRANSACTION WHERE USER_ID = ?",
+ (rs, rowNum) ->
+ new Transaction(
+ rs.getLong("id"),
+ rs.getInt("user_id"),
+ rs.getInt("crypto_currency_id"),
+ rs.getBigDecimal("amount"),
+ OperationType.valueOf(rs.getString("operation_type")),
+ rs.getString("transaction_date")),
+ userId);
+ }
+
+ public void saveTransaction(Integer userId, Integer cryptocurrencyId, BigDecimal amount, OperationType operationType, LocalDateTime transactionDate) {
+ jdbcTemplate.update("INSERT INTO TRANSACTION(USER_ID, CRYPTO_CURRENCY_ID, AMOUNT, OPERATION_TYPE, TRANSACTION_DATE) VALUES(?, ?, ?, ?, ?)",
+ userId, cryptocurrencyId, amount, operationType.name(), transactionDate.toString());
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/repositories/UserRepository.java b/src/main/java/com/serdeliverance/cryptowallet/repositories/UserRepository.java
new file mode 100644
index 0000000..7155fa9
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/repositories/UserRepository.java
@@ -0,0 +1,51 @@
+package com.serdeliverance.cryptowallet.repositories;
+
+import com.serdeliverance.cryptowallet.domain.User;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+import java.util.Optional;
+
+@Repository
+public class UserRepository {
+
+ private JdbcTemplate jdbcTemplate;
+
+ public UserRepository(JdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ private RowMapper userRowMapper = (rs, rowNum) -> new User(Optional.of(rs.getInt("ID")), rs.getString("USERNAME"), rs.getString("PASSWORD"), rs.getString("EMAIL"));
+
+ public Optional find(Integer id) {
+ return jdbcTemplate.query(
+ "SELECT ID, USERNAME, PASSWORD, EMAIL FROM USERS WHERE ENABLED = TRUE AND ID = ?",
+ userRowMapper, id).stream().findFirst();
+ }
+
+ public List getAll() {
+ return jdbcTemplate.query("SELECT ID, USERNAME, PASSWORD, EMAIL FROM USERS WHERE ENABLED = TRUE", userRowMapper);
+ }
+
+ public void save(User user) {
+ jdbcTemplate.update(
+ "INSERT INTO USERS(USERNAME, PASSWORD, EMAIL) VALUES(?, ?, ?)",
+ user.getUsername(),
+ user.getPassword(),
+ user.getEmail());
+ }
+
+ public void update(User user) {
+ jdbcTemplate.update("UPDATE USERS SET USERNAME = ?, PASSWORD = ?, EMAIL = ? WHERE ID = ?",
+ user.getUsername(),
+ user.getPassword(),
+ user.getEmail(),
+ user.getId().get());
+ }
+
+ public void delete(Integer userId) {
+ jdbcTemplate.update("UPDATE USERS SET ENABLED = FALSE WHERE ID = ?", userId);
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/services/CryptocurrencyService.java b/src/main/java/com/serdeliverance/cryptowallet/services/CryptocurrencyService.java
new file mode 100644
index 0000000..8c45f97
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/services/CryptocurrencyService.java
@@ -0,0 +1,46 @@
+package com.serdeliverance.cryptowallet.services;
+
+import com.serdeliverance.cryptowallet.clients.CoinmarketCapClient;
+import com.serdeliverance.cryptowallet.domain.Cryptocurrency;
+import com.serdeliverance.cryptowallet.dto.CurrencyQuoteDTO;
+import com.serdeliverance.cryptowallet.exceptions.ResourceNotFoundException;
+import com.serdeliverance.cryptowallet.repositories.CryptocurrencyRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+import static com.serdeliverance.cryptowallet.converters.CurrencyQuoteDTOConverter.convertFromResponse;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class CryptocurrencyService {
+
+ private final CoinmarketCapClient coinmarketCapClient;
+ private final CryptocurrencyRepository cryptocurrencyRepository;
+
+ public List quotes() {
+ log.info("Getting quotes");
+ return convertFromResponse(coinmarketCapClient.quotes());
+ }
+
+ public List getByIdList(List ids) {
+ log.info("Getting all cryptocurrencies with ids: {}", ids);
+ return cryptocurrencyRepository.getByIdList(ids);
+ }
+
+ public Cryptocurrency getByName(String cryptocurrency) {
+ log.info("Searching cryptocurrency: {}", cryptocurrency);
+ return cryptocurrencyRepository
+ .getByName(cryptocurrency)
+ .orElseThrow(() -> new ResourceNotFoundException("not found crypto currency with name: " + cryptocurrency));
+ }
+
+ public CurrencyQuoteDTO getQuote(String cryptocurrency) {
+ return quotes().stream()
+ .filter(crypto -> crypto.getCrypto().equals(cryptocurrency))
+ .findFirst().orElseThrow(() -> new ResourceNotFoundException("not found quote for cryptocurrency: " + cryptocurrency));
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/services/PortfolioService.java b/src/main/java/com/serdeliverance/cryptowallet/services/PortfolioService.java
new file mode 100644
index 0000000..959b28b
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/services/PortfolioService.java
@@ -0,0 +1,71 @@
+package com.serdeliverance.cryptowallet.services;
+
+import com.serdeliverance.cryptowallet.domain.Cryptocurrency;
+import com.serdeliverance.cryptowallet.domain.Transaction;
+import com.serdeliverance.cryptowallet.dto.CurrencyQuoteDTO;
+import com.serdeliverance.cryptowallet.dto.CurrencyTotalDTO;
+import com.serdeliverance.cryptowallet.dto.PorfolioDTO;
+import com.serdeliverance.cryptowallet.exceptions.InvalidOperationException;
+import com.serdeliverance.cryptowallet.repositories.TransactionRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static com.serdeliverance.cryptowallet.dto.PorfolioDTO.emptyPortfolio;
+import static java.util.stream.Collectors.groupingBy;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class PortfolioService {
+
+ private final UserService userService;
+ private final CryptocurrencyService cryptocurrencyService;
+ private final TransactionRepository transactionRepository;
+
+ public PorfolioDTO getPortfolio(Integer userId) {
+ log.info("Getting portfolio for userId: {}", userId);
+ userService.validateUser(userId);
+ List transactions = transactionRepository.getByUser(userId);
+ return !transactions.isEmpty() ? buildPorfolio(userId, transactions) : emptyPortfolio(userId, LocalDateTime.now());
+ }
+
+ private PorfolioDTO buildPorfolio(Integer userId, List transactions) {
+ log.debug("Building crypto portfolio");
+ Map quotesInUSD = cryptocurrencyService.quotes().stream()
+ .collect(Collectors.toMap(CurrencyQuoteDTO::getCrypto, crypto -> crypto.getQuoteInUsd()));
+ Map cryptoMap = cryptocurrencyService
+ .getByIdList(transactions.stream().map(tx -> tx.getCryptocurrencyId()).distinct().collect(Collectors.toList()))
+ .stream()
+ .collect(Collectors.toMap(Cryptocurrency::getId, crypto -> crypto.getName()));
+ List currencies = transactions.stream()
+ .collect(groupingBy(Transaction::getCryptocurrencyId))
+ .entrySet().stream()
+ .map(entry ->
+ new CurrencyTotalDTO(
+ cryptoMap.get(entry.getKey()),
+ entry.getValue().stream()
+ .map(tx -> tx.getAmount())
+ .reduce(BigDecimal.ZERO, BigDecimal::add)))
+ .collect(Collectors.toList());
+ BigDecimal totalInUSD = currencies.stream()
+ .map(crypto -> crypto.getAmount().multiply(quotesInUSD.get(crypto.getCurrency()))).reduce(BigDecimal.ZERO, BigDecimal::add);
+ return new PorfolioDTO(userId, currencies, totalInUSD, LocalDateTime.now());
+ }
+
+ public void validateFunds(Integer userId, String cryptocurrency, BigDecimal amount) {
+ log.info("Validating funds for selling/transferring data. userId={}, cryptocurrency={}, amount={}", userId, cryptocurrency, amount);
+ BigDecimal currencyTotal = transactionRepository
+ .getByUser(userId)
+ .stream()
+ .filter(tx -> tx.getCryptocurrencyId().equals(cryptocurrencyService.getByName(cryptocurrency).getId()))
+ .map(Transaction::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
+ if (currencyTotal.compareTo(amount) < 0) throw new InvalidOperationException("Insufficient funds for transference/selling");
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/services/TransactionService.java b/src/main/java/com/serdeliverance/cryptowallet/services/TransactionService.java
new file mode 100644
index 0000000..cb8568f
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/services/TransactionService.java
@@ -0,0 +1,75 @@
+package com.serdeliverance.cryptowallet.services;
+
+import com.serdeliverance.cryptowallet.domain.Cryptocurrency;
+import com.serdeliverance.cryptowallet.domain.Transaction;
+import com.serdeliverance.cryptowallet.dto.TransactionDTO;
+import com.serdeliverance.cryptowallet.repositories.TransactionRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+import static com.serdeliverance.cryptowallet.domain.OperationType.*;
+import static java.util.Collections.EMPTY_LIST;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toMap;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class TransactionService {
+
+ private final TransactionRepository transactionRepository;
+ private final CryptocurrencyService cryptocurrencyService;
+ private final UserService userService;
+ private final PortfolioService portfolioService;
+
+ public List getHistory(Integer userId) {
+ log.info("Getting transaction history for userId: {}", userId);
+ userService.validateUser(userId);
+ List transactions = transactionRepository.getByUser(userId);
+ return transactions.isEmpty() ? EMPTY_LIST : buildHistory(transactions);
+ }
+
+ private List buildHistory(List transactions) {
+ Map cryptoMap = cryptocurrencyService
+ .getByIdList(transactions.stream().map(Transaction::getCryptocurrencyId).distinct().collect(toList()))
+ .stream()
+ .collect(toMap(Cryptocurrency::getId, Cryptocurrency::getName));
+
+ return transactions.stream()
+ .map(tx -> new TransactionDTO(tx.getId(), cryptoMap.get(tx.getCryptocurrencyId()), tx.getAmount(), tx.getOperationType().name(), tx.getTransactionDate()))
+ .collect(toList());
+ }
+
+ public void transfer(Integer issuer, Integer receiver, String cryptocurrency, BigDecimal amount) {
+ log.info("Transferring {} {} from user: {} to user: {}", amount, cryptocurrency, issuer, receiver);
+ userService.validateUser(issuer);
+ userService.validateUser(receiver);
+ portfolioService.validateFunds(issuer, cryptocurrency, amount);
+ Integer cryptoCurrencyId = cryptocurrencyService.getByName(cryptocurrency).getId();
+ transactionRepository.saveTransaction(issuer, cryptoCurrencyId, amount.multiply(BigDecimal.valueOf(-1)), TRANSFERENCE, LocalDateTime.now());
+ transactionRepository.saveTransaction(receiver, cryptoCurrencyId, amount, BUY, LocalDateTime.now());
+ }
+
+ public void buy(Integer userId, String cryptocurrency, BigDecimal amountInUsd) {
+ log.info("Buying {} for an amount of {} dollars by user: {}", cryptocurrency, amountInUsd, userId);
+ userService.validateUser(userId);
+ Integer cryptoCurrencyId = cryptocurrencyService.getByName(cryptocurrency).getId();
+ BigDecimal quote = cryptocurrencyService.getQuote(cryptocurrency).getQuoteInUsd();
+ transactionRepository.saveTransaction(userId, cryptoCurrencyId, amountInUsd.divide(quote, 10, RoundingMode.CEILING), BUY, LocalDateTime.now());
+ }
+
+ public void sell(Integer userId, String cryptocurrency, BigDecimal amount) {
+ log.info("Selling {} {} by user: {}", amount, cryptocurrency, userId);
+ userService.validateUser(userId);
+ portfolioService.validateFunds(userId, cryptocurrency, amount);
+ transactionRepository.saveTransaction(userId, cryptocurrencyService.getByName(cryptocurrency).getId(),
+ amount.multiply(BigDecimal.valueOf(-1)), SELL, LocalDateTime.now());
+ }
+}
diff --git a/src/main/java/com/serdeliverance/cryptowallet/services/UserService.java b/src/main/java/com/serdeliverance/cryptowallet/services/UserService.java
new file mode 100644
index 0000000..8b2e672
--- /dev/null
+++ b/src/main/java/com/serdeliverance/cryptowallet/services/UserService.java
@@ -0,0 +1,56 @@
+package com.serdeliverance.cryptowallet.services;
+
+import com.serdeliverance.cryptowallet.domain.User;
+import com.serdeliverance.cryptowallet.exceptions.ResourceNotFoundException;
+import com.serdeliverance.cryptowallet.repositories.UserRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Optional;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class UserService {
+
+ private final UserRepository userRepository;
+
+ public Optional get(Integer id) {
+ log.info("Getting user with id: {}", id);
+ return userRepository.find(id);
+ }
+
+ public List getAll() {
+ log.info("Getting all users");
+ return userRepository.getAll();
+ }
+
+ public void create(User user) {
+ log.info("Creating user");
+ userRepository.save(user);
+ }
+
+ public void update(User user) {
+ Integer userId = user.getId().get();
+ if (this.exists(userId)) {
+ userRepository.update(user);
+ } else throw new ResourceNotFoundException("user: " + userId);
+ }
+
+ public void delete(Integer userId) {
+ if (this.exists(userId)) {
+ userRepository.delete(userId);
+ } else throw new ResourceNotFoundException("user: " + userId);
+ }
+
+ public void validateUser(Integer userId) {
+ log.info("Validating user {}", userId);
+ get(userId).orElseThrow(() -> new ResourceNotFoundException("user not found"));
+ }
+
+ private boolean exists(Integer id) {
+ return this.get(id).isPresent();
+ }
+}
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 8b13789..5fae0d5 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,7 @@
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.datasource.url=jdbc:postgresql://127.0.0.1:45432/cryptodb
+spring.datasource.username=root
+spring.datasource.password=root
+coinmarketcap.api-key=${COINMARKETCAP_API_KEY}
+coinmarketcap.url=${COINMARKETCAP_URL}
\ No newline at end of file
diff --git a/src/test/java/com/serdeliverance/cryptowallet/CryptoWalletApplicationTests.java b/src/test/java/com/serdeliverance/cryptowallet/CryptoWalletApplicationTests.java
deleted file mode 100644
index 026415f..0000000
--- a/src/test/java/com/serdeliverance/cryptowallet/CryptoWalletApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package com.serdeliverance.cryptowallet;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class CryptoWalletApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}
diff --git a/src/test/java/com/serdeliverance/cryptowallet/services/PortfolioServiceTest.java b/src/test/java/com/serdeliverance/cryptowallet/services/PortfolioServiceTest.java
new file mode 100644
index 0000000..ca174f3
--- /dev/null
+++ b/src/test/java/com/serdeliverance/cryptowallet/services/PortfolioServiceTest.java
@@ -0,0 +1,186 @@
+package com.serdeliverance.cryptowallet.services;
+
+import com.serdeliverance.cryptowallet.domain.Cryptocurrency;
+import com.serdeliverance.cryptowallet.domain.Transaction;
+import com.serdeliverance.cryptowallet.dto.CurrencyQuoteDTO;
+import com.serdeliverance.cryptowallet.dto.CurrencyTotalDTO;
+import com.serdeliverance.cryptowallet.dto.PorfolioDTO;
+import com.serdeliverance.cryptowallet.exceptions.InvalidOperationException;
+import com.serdeliverance.cryptowallet.exceptions.ResourceNotFoundException;
+import com.serdeliverance.cryptowallet.repositories.TransactionRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.math.BigDecimal;
+
+import static com.serdeliverance.cryptowallet.domain.OperationType.BUY;
+import static java.util.Arrays.asList;
+import static java.util.Collections.EMPTY_LIST;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+public class PortfolioServiceTest {
+
+ @Mock
+ private UserService userService;
+
+ @Mock
+ private CryptocurrencyService cryptocurrencyService;
+
+ @Mock
+ private TransactionRepository transactionRepository;
+
+ private PortfolioService portfolioService;
+
+ @BeforeEach
+ void setup() {
+ portfolioService = new PortfolioService(userService, cryptocurrencyService, transactionRepository);
+ }
+
+ @Test
+ void whenHasNoTransactionItShouldReturnEmptyPorfolio() {
+ // given
+ Integer userId = 1;
+
+ when(transactionRepository.getByUser(userId)).thenReturn(EMPTY_LIST);
+
+ // when
+ PorfolioDTO result = portfolioService.getPortfolio(userId);
+
+ // then
+ assertThat(result).isNotNull();
+ assertThat(result.getTotalInUSD()).isEqualTo(BigDecimal.ZERO);
+ assertThat(result.getCurrencies()).isEqualTo(EMPTY_LIST);
+ }
+
+ @Test
+ void whenUserHasTransactionsWithOneCurrencyItShouldReturnPorfolio() {
+ // given
+ Integer userId = 1;
+
+ when(transactionRepository.getByUser(userId)).thenReturn(asList(
+ new Transaction(23L, 1, 1, BigDecimal.valueOf(2), BUY, "2021-02-05T19:29:03.239"),
+ new Transaction(26L, 1, 1, BigDecimal.ONE, BUY, "2021-02-06T19:29:03.239"))
+ );
+ when(cryptocurrencyService.quotes()).thenReturn(asList(
+ new CurrencyQuoteDTO("Bitcoin", BigDecimal.valueOf(48081.979491230726)),
+ new CurrencyQuoteDTO("Ethereum", BigDecimal.valueOf(1810.264184795378)),
+ new CurrencyQuoteDTO("Tether", BigDecimal.valueOf(1.00048112681234)),
+ new CurrencyQuoteDTO("Cardano", BigDecimal.valueOf(0.87957104579375)))
+ );
+ when(cryptocurrencyService.getByIdList(asList(1))).thenReturn(singletonList(new Cryptocurrency(1, "Bitcoin", "BTC")));
+
+ // when
+ PorfolioDTO result = portfolioService.getPortfolio(userId);
+
+ // then
+ BigDecimal expectedTotalInUsd = BigDecimal.valueOf(48081.979491230726).multiply(BigDecimal.valueOf(3));
+ CurrencyTotalDTO expectedCurrency = new CurrencyTotalDTO("Bitcoin", BigDecimal.valueOf(3));
+
+ assertThat(result).isNotNull();
+ assertThat(result.getUserId()).isEqualTo(userId);
+ assertThat(result.getCurrencies().get(0)).isEqualTo(expectedCurrency);
+ assertThat(result.getTotalInUSD()).isEqualTo(expectedTotalInUsd);
+ }
+
+ @Test
+ void whenUserHasTransactionsWithMultipleCurrenciesItShouldReturnPorfolio() {
+ // given
+ Integer userId = 1;
+
+ BigDecimal bitcoinQuote = BigDecimal.valueOf(48081.979491230726);
+ BigDecimal ethereumQuote = BigDecimal.valueOf(1810.264184795378);
+ BigDecimal tetherQuote = BigDecimal.valueOf(1.00048112681234);
+
+ when(transactionRepository.getByUser(userId)).thenReturn(asList(
+ new Transaction(23L, 1, 1, BigDecimal.valueOf(2), BUY, "2021-02-05T19:29:03.239"),
+ new Transaction(26L, 1, 1, BigDecimal.ONE, BUY, "2021-02-06T19:29:03.239"),
+ new Transaction(27L, 1, 2, BigDecimal.ONE, BUY, "2021-02-06T19:29:03.239"),
+ new Transaction(34L, 1, 3, BigDecimal.valueOf(2), BUY, "2021-02-06T19:29:03.239"),
+ new Transaction(78L, 1, 2, BigDecimal.ONE, BUY, "2021-02-06T19:29:03.239"))
+ );
+ when(cryptocurrencyService.quotes()).thenReturn(asList(
+ new CurrencyQuoteDTO("Bitcoin", bitcoinQuote),
+ new CurrencyQuoteDTO("Ethereum", ethereumQuote),
+ new CurrencyQuoteDTO("Tether", tetherQuote),
+ new CurrencyQuoteDTO("Cardano", BigDecimal.valueOf(0.87957104579375)))
+ );
+ when(cryptocurrencyService.getByIdList(asList(1, 2, 3))).thenReturn(asList(
+ new Cryptocurrency(1, "Bitcoin", "BTC"),
+ new Cryptocurrency(2, "Ethereum", "ETH"),
+ new Cryptocurrency(3, "Tether", "TET"))
+ );
+
+ // when
+ PorfolioDTO result = portfolioService.getPortfolio(userId);
+
+ CurrencyTotalDTO resultBitcoinTotal = result.getCurrencies().stream().filter(c -> c.getCurrency().equals("Bitcoin")).findFirst().get();
+ CurrencyTotalDTO resultEthereumTotal = result.getCurrencies().stream().filter(c -> c.getCurrency().equals("Ethereum")).findFirst().get();
+ CurrencyTotalDTO resultTetherTotal = result.getCurrencies().stream().filter(c -> c.getCurrency().equals("Tether")).findFirst().get();
+
+ // then
+ CurrencyTotalDTO expectedBitcoinTotal = new CurrencyTotalDTO("Bitcoin", BigDecimal.valueOf(3));
+ CurrencyTotalDTO expectedEthereumTotal = new CurrencyTotalDTO("Ethereum", BigDecimal.valueOf(2));
+ CurrencyTotalDTO expectedTetherTotal = new CurrencyTotalDTO("Tether", BigDecimal.valueOf(2));
+
+ BigDecimal expectedBitcoinInUsd = expectedBitcoinTotal.getAmount().multiply(bitcoinQuote);
+ BigDecimal expectedEthereumInUsd = expectedEthereumTotal.getAmount().multiply(ethereumQuote);
+ BigDecimal expectedTetherInUsd = expectedTetherTotal.getAmount().multiply(tetherQuote);
+
+ BigDecimal expectedTotalInUsd = expectedBitcoinInUsd.add(expectedEthereumInUsd).add(expectedTetherInUsd);
+
+ assertThat(result).isNotNull();
+ assertThat(resultBitcoinTotal).isEqualTo(expectedBitcoinTotal);
+ assertThat(resultEthereumTotal).isEqualTo(expectedEthereumTotal);
+ assertThat(resultTetherTotal).isEqualTo(expectedTetherTotal);
+ assertThat(result.getTotalInUSD()).isEqualTo(expectedTotalInUsd);
+ }
+
+ @Test
+ public void whenUserNotExistsItShouldThrowResourceNotFoundException() {
+ // given
+ Integer userId = 12;
+
+ doThrow(ResourceNotFoundException.class).when(userService).validateUser(userId);
+
+ // then
+ assertThrows(ResourceNotFoundException.class, () -> portfolioService.getPortfolio(userId));
+ }
+
+ @Test
+ public void whenUserHasNotEnoughFundsValidateShouldThrowException() {
+ // given
+ when(cryptocurrencyService.getByName("Bitcoin")).thenReturn(new Cryptocurrency(1, "Bitcoin", "BTC"));
+ when(transactionRepository.getByUser(2))
+ .thenReturn(asList(
+ new Transaction(23L, 2, 1, BigDecimal.ONE, BUY, "2021-02-05T19:29:03.239"),
+ new Transaction(26L, 2, 1, BigDecimal.ONE, BUY, "2021-02-06T19:29:03.239"),
+ new Transaction(27L, 2, 1, BigDecimal.ONE, BUY, "2021-02-06T19:29:03.239"))
+ );
+
+ // when/then
+ assertThrows(InvalidOperationException.class, () -> portfolioService.validateFunds(2, "Bitcoin", BigDecimal.valueOf(10)));
+ }
+
+ @Test
+ public void whenUserHasFundsItShouldReturnVoid() {
+ // given
+ when(cryptocurrencyService.getByName("Bitcoin")).thenReturn(new Cryptocurrency(1, "Bitcoin", "BTC"));
+ when(transactionRepository.getByUser(2))
+ .thenReturn(asList(
+ new Transaction(23L, 2, 1, BigDecimal.ONE, BUY, "2021-02-05T19:29:03.239"),
+ new Transaction(26L, 2, 1, BigDecimal.ONE, BUY, "2021-02-06T19:29:03.239"),
+ new Transaction(27L, 2, 1, BigDecimal.ONE, BUY, "2021-02-06T19:29:03.239"))
+ );
+
+ // when/then
+ portfolioService.validateFunds(2, "Bitcoin", BigDecimal.ONE);
+ }
+}
diff --git a/src/test/java/com/serdeliverance/cryptowallet/services/TransactionServiceTest.java b/src/test/java/com/serdeliverance/cryptowallet/services/TransactionServiceTest.java
new file mode 100644
index 0000000..c0879f4
--- /dev/null
+++ b/src/test/java/com/serdeliverance/cryptowallet/services/TransactionServiceTest.java
@@ -0,0 +1,150 @@
+package com.serdeliverance.cryptowallet.services;
+
+import com.serdeliverance.cryptowallet.domain.Cryptocurrency;
+import com.serdeliverance.cryptowallet.domain.Transaction;
+import com.serdeliverance.cryptowallet.dto.TransactionDTO;
+import com.serdeliverance.cryptowallet.exceptions.InvalidOperationException;
+import com.serdeliverance.cryptowallet.exceptions.ResourceNotFoundException;
+import com.serdeliverance.cryptowallet.repositories.TransactionRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+import static com.serdeliverance.cryptowallet.domain.OperationType.BUY;
+import static com.serdeliverance.cryptowallet.domain.OperationType.SELL;
+import static java.util.Arrays.asList;
+import static java.util.Collections.EMPTY_LIST;
+import static java.util.Collections.singletonList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+public class TransactionServiceTest {
+
+ @Mock
+ private UserService userService;
+
+ @Mock
+ private TransactionRepository transactionRepository;
+
+ @Mock
+ private CryptocurrencyService cryptocurrencyService;
+
+ @Mock
+ private PortfolioService portfolioService;
+
+ private TransactionService transactionService;
+
+ @BeforeEach
+ void setup() {
+ transactionService = new TransactionService(transactionRepository, cryptocurrencyService, userService, portfolioService);
+ }
+
+ @Test
+ public void whenUserNotExistsItShouldThrowResourceNotFoundException() {
+ // given
+ Integer userId = 12;
+
+ doThrow(ResourceNotFoundException.class).when(userService).validateUser(userId);
+
+ // then
+ assertThrows(ResourceNotFoundException.class, () -> transactionService.getHistory(userId));
+
+ }
+
+ @Test
+ public void whenUserHasNoTransactionItShouldReturnEmptyList() {
+ // given
+ Integer userId = 2;
+
+ when(transactionRepository.getByUser(userId)).thenReturn(EMPTY_LIST);
+
+ // when
+ List result = transactionService.getHistory(userId);
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void whenUserHasOneTransactionItShouldReturnTransactionHistory() {
+ // given
+ Integer userId = 1;
+
+ when(transactionRepository.getByUser(userId)).thenReturn(singletonList(
+ new Transaction(12L, 1, 1, BigDecimal.valueOf(2), BUY, "2021-02-05T19:28:43.111")
+ ));
+
+ when(cryptocurrencyService.getByIdList(singletonList(1))).thenReturn(asList(
+ new Cryptocurrency(1, "Bitcoin", "BTC"))
+ );
+
+ // when
+ List result = transactionService.getHistory(userId);
+
+ // then
+ assertThat(result).isNotNull();
+ assertThat(result.size()).isEqualTo(1);
+ assertThat(result.get(0).getCryptocurrency()).isEqualTo("Bitcoin");
+ }
+
+ @Test
+ public void whenUserTransactionsItShouldReturnTransactionHistory() {
+ // given
+ Integer userId = 1;
+
+ when(transactionRepository.getByUser(userId)).thenReturn(asList(
+ new Transaction(12L, 1, 1, BigDecimal.valueOf(2), BUY, "2021-02-05T19:28:43.111"),
+ new Transaction(13L, 1, 1, BigDecimal.valueOf(1), SELL, "2021-02-05T19:28:43.111"),
+ new Transaction(14L, 1, 2, BigDecimal.valueOf(1), BUY, "2021-02-05T19:28:43.111")
+ ));
+
+ when(cryptocurrencyService.getByIdList(asList(1, 2))).thenReturn(asList(
+ new Cryptocurrency(1, "Bitcoin", "BTC"),
+ new Cryptocurrency(2, "Ethereum", "ETH"))
+ );
+
+ // when
+ List result = transactionService.getHistory(userId);
+
+ // then
+ assertThat(result).isNotNull();
+ assertThat(result.size()).isEqualTo(3);
+ }
+
+ @Test
+ public void whenTransferWithInvalidUserItShouldThrowInvalidOperationException() {
+ // given
+ doNothing().when(userService).validateUser(12);
+ doThrow(ResourceNotFoundException.class).when(userService).validateUser(2);
+
+ // when/then
+ assertThrows(ResourceNotFoundException.class, () ->
+ transactionService.transfer(12, 2, "Bitcoin", BigDecimal.ONE));
+ }
+
+ @Test
+ public void whenUserAmountIsInvalidItShouldThrowInvalidaOperationException() {
+ // given
+ doThrow(InvalidOperationException.class).when(portfolioService).validateFunds(12, "Bitcoin", BigDecimal.ONE);
+
+ // when/then
+ assertThrows(InvalidOperationException.class, () ->
+ transactionService.transfer(12, 2, "Bitcoin", BigDecimal.ONE));
+ }
+
+ @Test
+ public void whenUserTransferItShouldTransferOk() {
+ // given
+ when(cryptocurrencyService.getByName("Bitcoin")).thenReturn(new Cryptocurrency(1, "Bitcoin", "BTC"));
+
+ // when/then
+ transactionService.transfer(12, 2, "Bitcoin", BigDecimal.ONE);
+ }
+}