diff --git a/http/PaymentSystem/.gitignore b/http/PaymentSystem/.gitignore
new file mode 100644
index 0000000..ce8e2ae
--- /dev/null
+++ b/http/PaymentSystem/.gitignore
@@ -0,0 +1,109 @@
+
+# Created by https://www.gitignore.io/api/java,intellij
+# Edit at https://www.gitignore.io/?templates=java,intellij
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+*.iml
+ User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+#auto-import.
+.idea/modules.xml
+.idea/*.iml
+.idea/modules
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+
+modules.xml
+.idea/misc.xml
+*.ipr
+*.iml
+# Sonarlint plugin
+.idea/sonarlint
+
+### Java ###
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+# End of https://www.gitignore.io/api/java,intellij
\ No newline at end of file
diff --git a/http/PaymentSystem/PaymentSystem.iml b/http/PaymentSystem/PaymentSystem.iml
new file mode 100644
index 0000000..78b2cc5
--- /dev/null
+++ b/http/PaymentSystem/PaymentSystem.iml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/http/PaymentSystem/pom.xml b/http/PaymentSystem/pom.xml
new file mode 100644
index 0000000..2343c03
--- /dev/null
+++ b/http/PaymentSystem/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+
+ payment-system
+ payment-system
+ 1.0-SNAPSHOT
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 1.8
+ 1.8
+
+
+
+
+
+
+
+ com.google.code.findbugs
+ jsr305
+ 3.0.2
+
+
+ org.slf4j
+ slf4j-api
+ 1.7.26
+
+
+ commons-io
+ commons-io
+ 2.5
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ 2.9.8
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.9.8
+
+
+
\ No newline at end of file
diff --git a/http/PaymentSystem/readme.md b/http/PaymentSystem/readme.md
new file mode 100644
index 0000000..1221190
--- /dev/null
+++ b/http/PaymentSystem/readme.md
@@ -0,0 +1,135 @@
+# Запросы
+### Добавить пользователя
+POST /user\
+input:
+```json
+{
+ "login": "alol",
+ "password": "123"
+}
+```
+output:
+```json
+{
+ "status": "OK",
+ "description": "description"
+}
+```
+
+
+### Выдать все кошельки для всех пользователей
+GET /users\
+output: - массив логинов
+```json
+["user1", "user2", "user3"]
+```
+
+### Перевод от одного пользователя к другому
+POST /send\
+input:
+```json
+{
+ "fromLogin":"loginFrom",
+ "fromPassword":"passwordTo",
+ "toLogin":"loginTo",
+ "count":20
+}
+```
+
+output:
+```json
+{
+ "status": "OK",
+ "description": "description"
+}
+```
+
+
+### Запросить перевод
+POST /send
+input:
+```json
+{
+ "fromLogin":"loginFrom",
+ "fromPassword":"passwordTo",
+ "toLogin":"loginTo",
+ "count":20
+}
+```
+
+output:
+ ```json
+ {
+ "status": "OK",
+ "description": "",
+ "entityId": "p626u7copf2tha8rc3yz"
+}
+```
+
+### Реакция на запрос перевода перевода
+POST /accept
+input
+```json
+{
+ "login":"login",
+ "password":"123",
+ "key":"qe2bmngpw41niaa6junt",
+ "actionType":"ACCEPT"
+}
+```
+output:
+```json
+{
+ "status": "OK",
+ "description": ""
+}
+```
+
+### Проверить состояние кошелька
+POST /wallet
+input:
+```json
+{
+ "login": "alol",
+ "password": "123"
+}
+```
+output:
+```json
+{
+ "login": "olya2",
+ "count": 88
+}
+```
+
+### Просмотреть все запросы
+POST /request
+input:
+```json
+{
+ "login": "alol",
+ "password": "123"
+}
+```
+output:
+```json
+[
+ {
+ "from": "fromLogin",
+ "key": "ulrrp3ebqfq0nsrjdaz1",
+ "count": 30
+ },
+ {
+ "from": "user2",
+ "key": "5sqwz36u0gtf0v91mxfx",
+ "count": 20
+ }
+]
+```
+
+
+
+
+
+
+
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ClientHandler.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ClientHandler.java
new file mode 100644
index 0000000..38a9012
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ClientHandler.java
@@ -0,0 +1,156 @@
+package ru.hse.alyokhina.server;
+
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import ru.hse.alyokhina.server.dto.*;
+import ru.hse.alyokhina.server.repository.PaymentSystemRepository;
+import ru.hse.alyokhina.server.exception.NotAuthorizedException;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nonnull;
+
+
+import static ru.hse.alyokhina.server.ReadWriteHelper.readBody;
+import static ru.hse.alyokhina.server.ReadWriteHelper.writeResponse;
+import static ru.hse.alyokhina.server.ReadWriteHelper.ContentType;
+import static ru.hse.alyokhina.server.ReadWriteHelper.readHeaders;
+import static ru.hse.alyokhina.server.ReadWriteHelper.readPath;
+
+
+public class ClientHandler implements Runnable {
+ private final Socket client;
+ private final ObjectMapper _J = new ObjectMapper();
+ private final PaymentSystemRepository paymentSystem = new PaymentSystemRepository();
+
+ public ClientHandler(@Nonnull Socket client) {
+ this.client = client;
+ }
+
+ public void run() {
+ try {
+ final BufferedReader inReader = new BufferedReader(
+ new InputStreamReader(client.getInputStream(), "UTF-8"));
+ final BufferedWriter outWriter = new BufferedWriter(
+ new OutputStreamWriter(client.getOutputStream(), "UTF-8"));
+ String response;
+ String status;
+ ContentType contentType;
+ try {
+ final String path = readPath(inReader);
+ final Map headers = readHeaders(inReader);
+ final String body = readBody(inReader, headers);
+ System.out.println(path);
+ System.out.println(headers);
+ System.out.println(body);
+ response = apply(path, body);
+ status = "200";
+ contentType = ContentType.JSON;
+ } catch (IllegalArgumentException e) {
+ status = "400";
+ response = e.getMessage();
+ contentType = ContentType.TEXT;
+ } catch (NotAuthorizedException e) {
+ status = "401";
+ response = e.getMessage();
+ contentType = ContentType.TEXT;
+ } catch (ClassNotFoundException e) {
+ status = "404";
+ response = e.getMessage();
+ contentType = ContentType.TEXT;
+ } catch (Throwable e) {
+ status = "500";
+ response = e.getMessage();
+ contentType = ContentType.TEXT;
+ }
+ writeResponse(status, response, contentType, outWriter);
+ client.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+
+ private String apply(@Nonnull final String path, @Nonnull final String body) throws JsonProcessingException
+ , ClassNotFoundException
+ , NotAuthorizedException {
+ final String pathParts[] = path.split(" ");
+ if (pathParts.length < 2) {
+ throw new IllegalArgumentException("Failed to parse http path");
+ }
+ if (pathParts[0].toLowerCase().equals("post")
+ && pathParts[1].equals("/user")) {
+ UserInfo userInfo;
+ try {
+ userInfo = _J.readValue(body, UserInfo.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to parse body as UserInfo", e);
+ }
+ final DefaultResponse response = paymentSystem.createUser(userInfo);
+ return _J.writeValueAsString(response);
+ } else if (pathParts[0].toLowerCase().equals("get")
+ && pathParts[1].equals("/users")) {
+ return _J.writeValueAsString(paymentSystem.getUsers());
+ } else if (pathParts[0].toLowerCase().equals("post")
+ && pathParts[1].equals("/send")) {
+ TransferRequest transferRequest;
+ try {
+ transferRequest = _J.readValue(body, TransferRequest.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to parse body as TransferRequest", e);
+ }
+ final DefaultResponse response = paymentSystem.sendTransfer(transferRequest);
+ return _J.writeValueAsString(response);
+ } else if (pathParts[0].toLowerCase().equals("post")
+ && pathParts[1].equals("/request")) {
+ TransferRequest transferRequest;
+ try {
+ transferRequest = _J.readValue(body, TransferRequest.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to parse body as TransferRequest", e);
+ }
+ final DefaultResponse response = paymentSystem.requestTransfer(transferRequest);
+ return _J.writeValueAsString(response);
+ } else if (pathParts[0].toLowerCase().equals("post")
+ && pathParts[1].equals("/accept")) {
+ TransferRequestAccept transferRequestAccept;
+ try {
+ transferRequestAccept = _J.readValue(body, TransferRequestAccept.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to parse body as TransferRequestAccept", e);
+ }
+ final DefaultResponse response = paymentSystem.acceptRequest(transferRequestAccept);
+ return _J.writeValueAsString(response);
+ } else if (pathParts[0].toLowerCase().equals("post")
+ && pathParts[1].equals("/wallet")) {
+ UserInfo userInfo;
+ try {
+ userInfo = _J.readValue(body, UserInfo.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to parse body as UserInfo", e);
+ }
+ final WalletInfo response = paymentSystem.getCountMoney(userInfo);
+ return _J.writeValueAsString(response);
+ } else if (pathParts[0].toLowerCase().equals("post")
+ && pathParts[1].equals("/requests")) {
+ UserInfo userInfo;
+ try {
+ userInfo = _J.readValue(body, UserInfo.class);
+ } catch (IOException e) {
+ throw new IllegalArgumentException("Failed to parse body as UserInfo", e);
+ }
+ final List response = paymentSystem.getRequests(userInfo);
+ return _J.writeValueAsString(response);
+ }
+
+ throw new ClassNotFoundException(pathParts[0] + " " + pathParts[1] + " not found");
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Main.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Main.java
new file mode 100644
index 0000000..f7813da
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Main.java
@@ -0,0 +1,10 @@
+package ru.hse.alyokhina.server;
+
+import java.io.IOException;
+
+public class Main {
+ public static void main(String[] args) throws IOException {
+ final Server server = new Server(8000);
+ server.start();
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ReadWriteHelper.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ReadWriteHelper.java
new file mode 100644
index 0000000..b28f920
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/ReadWriteHelper.java
@@ -0,0 +1,77 @@
+package ru.hse.alyokhina.server;
+
+import javax.annotation.Nonnull;
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+public class ReadWriteHelper {
+ public enum ContentType {JSON, TEXT}
+
+ public static void writeResponse(@Nonnull final String statusCode,
+ @Nonnull final String body,
+ @Nonnull final ContentType contentType,
+ @Nonnull final BufferedWriter writer) throws IOException, NoSuchElementException {
+ writer.write("HTTP/1.1 " + statusCode + "\n");
+ writer.write("Content-Type: " + convertContentType(contentType) + "\n" +
+ "Content-Length: " + body.length() + "\n");
+ writer.write("\n");
+ writer.write(body);
+ writer.flush();
+ }
+
+ @Nonnull
+ public static String convertContentType(@Nonnull final ContentType contentType) throws NoSuchElementException {
+ switch (contentType) {
+ case JSON:
+ return "application/json";
+ case TEXT:
+ return "text/plain";
+ default:
+ throw new NoSuchElementException("Failed to convert " + contentType);
+ }
+ }
+
+ public static String readPath(@Nonnull final BufferedReader in) throws IOException {
+ return in.readLine();
+ }
+
+ @Nonnull
+ public static Map readHeaders(@Nonnull final BufferedReader in) throws IOException, IllegalArgumentException {
+ final Map headers = new HashMap();
+ while (true) {
+ final String curLine = in.readLine();
+ if ("".equals(curLine)) {
+ break;
+ }
+ final String[] keyAndValues = curLine.split(": ");
+ if (keyAndValues.length != 2) {
+ throw new IllegalArgumentException("Failed to parse http headers");
+ }
+ headers.put(keyAndValues[0].toLowerCase(), keyAndValues[1]);
+ }
+ return headers;
+ }
+
+ @Nonnull
+ public static String readBody(@Nonnull final BufferedReader in, @Nonnull final Map headers) throws IOException,
+ IllegalArgumentException {
+ if (headers.get("content-length") == null) {
+ return "";
+ }
+ try {
+ final int contentLength = Integer.parseInt(headers.get("content-length"));
+ final char[] buffer = new char[contentLength];
+ final int countRead = in.read(buffer);
+ if (countRead != contentLength) {
+ throw new IllegalArgumentException("Error during reading body, content-length = " + contentLength);
+ }
+ return new String(buffer);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Failed to parse content-length ", e);
+ }
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Server.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Server.java
new file mode 100644
index 0000000..c7e6d10
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/Server.java
@@ -0,0 +1,28 @@
+package ru.hse.alyokhina.server;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+import java.net.Socket;
+
+public class Server {
+ private final int port;
+ private final ServerSocket serverSocket;
+
+ public Server(int port) throws IOException {
+ this.port = port;
+ this.serverSocket = new ServerSocket(port);
+ }
+
+ public void start() throws IOException {
+ while (true) {
+ final Socket client = serverSocket.accept();
+ final Thread clientThread = new Thread(new ClientHandler(client));
+ clientThread.start();
+ }
+
+ }
+
+ public int getPort() {
+ return port;
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/ActionType.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/ActionType.java
new file mode 100644
index 0000000..6afff8e
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/ActionType.java
@@ -0,0 +1,5 @@
+package ru.hse.alyokhina.server.dto;
+
+public enum ActionType {
+ ACCEPT, CANCEL
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/DefaultResponse.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/DefaultResponse.java
new file mode 100644
index 0000000..ce600dc
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/DefaultResponse.java
@@ -0,0 +1,35 @@
+package ru.hse.alyokhina.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nonnull;
+
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DefaultResponse {
+ private final Status status;
+ private final String description;
+ private final String entityId;
+
+ @JsonCreator
+ public DefaultResponse(@JsonProperty("status") @Nonnull final Status status,
+ @JsonProperty("description") final String description,
+ @JsonProperty("entityId") final String entityId) {
+ this.status = status;
+ this.description = description;
+ this.entityId = entityId;
+ }
+
+ public Status getStatus() {
+ return status;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getEntityId() {
+ return entityId;
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/RequestInfo.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/RequestInfo.java
new file mode 100644
index 0000000..18bb856
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/RequestInfo.java
@@ -0,0 +1,33 @@
+package ru.hse.alyokhina.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nonnull;
+
+public class RequestInfo {
+ private final String from;
+ private final String key;
+ private final long count;
+
+ @JsonCreator
+ public RequestInfo(@JsonProperty("from") @Nonnull final String from,
+ @JsonProperty("key") @Nonnull final String key,
+ @JsonProperty("count") final long count) {
+ this.from = from;
+ this.key = key;
+ this.count = count;
+ }
+
+ public String getFrom() {
+ return from;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public long getCount() {
+ return count;
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/Status.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/Status.java
new file mode 100644
index 0000000..2810e23
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/Status.java
@@ -0,0 +1,5 @@
+package ru.hse.alyokhina.server.dto;
+
+public enum Status {
+ OK, ERROR
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequest.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequest.java
new file mode 100644
index 0000000..2c6d6d2
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequest.java
@@ -0,0 +1,41 @@
+package ru.hse.alyokhina.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nonnull;
+
+
+public class TransferRequest {
+ private final String fromLogin;
+ private final String toLogin;
+ private final String fromPassword;
+ private final long count;
+
+ @JsonCreator
+ public TransferRequest(@JsonProperty("fromLogin") @Nonnull final String fromLogin,
+ @JsonProperty("toLogin") @Nonnull final String toLogin,
+ @JsonProperty("fromPassword") @Nonnull final String fromPassword,
+ @JsonProperty("count") final long count) {
+ this.fromLogin = fromLogin;
+ this.toLogin = toLogin;
+ this.fromPassword = fromPassword;
+ this.count = count;
+ }
+
+ public String getFromLogin() {
+ return fromLogin;
+ }
+
+ public String getToLogin() {
+ return toLogin;
+ }
+
+ public String getFromPassword() {
+ return fromPassword;
+ }
+
+ public long getCount() {
+ return count;
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequestAccept.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequestAccept.java
new file mode 100644
index 0000000..f5176eb
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/TransferRequestAccept.java
@@ -0,0 +1,42 @@
+package ru.hse.alyokhina.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nonnull;
+
+public class TransferRequestAccept {
+
+ private final String login;
+ private final String password;
+ private final String key;
+ private final ActionType actionType;
+
+ @JsonCreator
+ public TransferRequestAccept(@JsonProperty("login") @Nonnull final String login,
+ @JsonProperty("password") @Nonnull final String password,
+ @JsonProperty("key") @Nonnull final String key,
+ @JsonProperty("actionType") @Nonnull ActionType actionType) {
+
+ this.login = login;
+ this.password = password;
+ this.key = key;
+ this.actionType = actionType;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public ActionType getActionType() {
+ return actionType;
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/UserInfo.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/UserInfo.java
new file mode 100644
index 0000000..b845636
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/UserInfo.java
@@ -0,0 +1,27 @@
+package ru.hse.alyokhina.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nonnull;
+
+public class UserInfo {
+ private final String login;
+ private final String password;
+
+ @JsonCreator
+ public UserInfo(@JsonProperty("login") @Nonnull final String login,
+ @JsonProperty("password") @Nonnull final String password) {
+
+ this.login = login;
+ this.password = password;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/WalletInfo.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/WalletInfo.java
new file mode 100644
index 0000000..0c0b2f9
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/dto/WalletInfo.java
@@ -0,0 +1,27 @@
+package ru.hse.alyokhina.server.dto;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import javax.annotation.Nonnull;
+
+public class WalletInfo {
+ private final String login;
+ private final Long count;
+
+ @JsonCreator
+ public WalletInfo(@JsonProperty("login") @Nonnull final String login,
+ @JsonProperty("count") @Nonnull final Long count) {
+
+ this.login = login;
+ this.count = count;
+ }
+
+ public String getLogin() {
+ return login;
+ }
+
+ public Long getCount() {
+ return count;
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/exception/NotAuthorizedException.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/exception/NotAuthorizedException.java
new file mode 100644
index 0000000..7520f69
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/exception/NotAuthorizedException.java
@@ -0,0 +1,7 @@
+package ru.hse.alyokhina.server.exception;
+
+public class NotAuthorizedException extends Exception {
+ public NotAuthorizedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/repository/PaymentSystemRepository.java b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/repository/PaymentSystemRepository.java
new file mode 100644
index 0000000..690556e
--- /dev/null
+++ b/http/PaymentSystem/src/main/java/ru/hse/alyokhina/server/repository/PaymentSystemRepository.java
@@ -0,0 +1,236 @@
+package ru.hse.alyokhina.server.repository;
+
+
+import ru.hse.alyokhina.server.dto.*;
+import ru.hse.alyokhina.server.exception.NotAuthorizedException;
+
+import javax.annotation.Nonnull;
+
+import java.util.*;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.stream.Collectors;
+
+public class PaymentSystemRepository {
+ private long defaultMoneyCount = 100;
+
+ private static ReadWriteLock readWriteLockData = new ReentrantReadWriteLock();
+ private static Lock readLockData = readWriteLockData.readLock();
+ private static Lock writeLockData = readWriteLockData.writeLock();
+
+ private static ReadWriteLock readWriteLockRequest = new ReentrantReadWriteLock();
+ private static Lock readLockRequest = readWriteLockRequest.readLock();
+ private static Lock writeLockRequest = readWriteLockRequest.writeLock();
+
+ private static Map data = new HashMap<>();
+ private static Map> requests = new HashMap<>();
+
+ @Nonnull
+
+ public DefaultResponse createUser(@Nonnull final UserInfo request) {
+ writeLockData.lock();
+ if (data.get(request.getLogin()) != null) {
+ writeLockData.unlock();
+ throw new IllegalArgumentException("This login " + request.getLogin() + " is already in use.");
+ }
+ final User newUser = new User(request.getLogin(), request.getPassword(), defaultMoneyCount);
+ data.put(request.getLogin(), newUser);
+ writeLockData.unlock();
+ writeLockRequest.lock();
+ requests.put(request.getLogin(), new HashMap<>());
+ writeLockRequest.unlock();
+ return new DefaultResponse(Status.OK, "", null);
+ }
+
+ @Nonnull
+ public Collection getUsers() {
+ readLockData.lock();
+ final Set users = data.keySet();
+ readLockData.unlock();
+ return users;
+ }
+
+ @Nonnull
+ public DefaultResponse sendTransfer(@Nonnull final TransferRequest transferRequest) throws NotAuthorizedException {
+ valid(transferRequest.getFromLogin(), transferRequest.getFromPassword(), transferRequest.getToLogin());
+ boolean result = transfer(transferRequest.getFromLogin(), transferRequest.getFromLogin(), transferRequest.getCount());
+ return result
+ ? new DefaultResponse(Status.OK, "", null)
+ : new DefaultResponse(Status.ERROR, "not enough money to transfer", null);
+ }
+
+
+ @Nonnull
+ public DefaultResponse requestTransfer(@Nonnull final TransferRequest transferRequest) throws NotAuthorizedException {
+ valid(transferRequest.getFromLogin(), transferRequest.getFromPassword(), transferRequest.getToLogin());
+ writeLockRequest.lock();
+ final Map requestsForTo = requests.get(transferRequest.getToLogin());
+ final String key = generateKey(requestsForTo);
+ requestsForTo.put(key, new Request(transferRequest.getFromLogin(), transferRequest.getCount()));
+ writeLockRequest.unlock();
+ return new DefaultResponse(Status.OK, "", key);
+ }
+
+ @Nonnull
+ public DefaultResponse acceptRequest(@Nonnull TransferRequestAccept requestAccept) throws NotAuthorizedException {
+ valid(requestAccept.getLogin(), requestAccept.getPassword(), null);
+
+ writeLockRequest.lock();
+ final Map requestsForUser = requests.get(requestAccept.getLogin());
+ final Request request = requestsForUser.get(requestAccept.getKey());
+ if (request == null) {
+ writeLockRequest.unlock();
+ return new DefaultResponse(Status.ERROR, "Request " + requestAccept.getKey() + " not found", null);
+ }
+ if (requestAccept.getActionType() == ActionType.ACCEPT) {
+ boolean result = transfer(requestAccept.getLogin(), request.loginFrom, request.count);
+ if (!result) {
+ writeLockRequest.unlock();
+ new DefaultResponse(Status.ERROR, "not enough money to transfer", null);
+ }
+ }
+ requestsForUser.remove(requestAccept.getKey());
+ writeLockRequest.unlock();
+ return new DefaultResponse(Status.OK, "", null);
+ }
+
+ @Nonnull
+ public WalletInfo getCountMoney(@Nonnull final UserInfo userInfo) throws NotAuthorizedException {
+ valid(userInfo.getLogin(), userInfo.getPassword(), null);
+ readLockData.lock();
+ long countMoney = data.get(userInfo.getLogin()).count;
+ readLockData.unlock();
+ return new WalletInfo(userInfo.getLogin(), countMoney);
+ }
+
+
+ @Nonnull
+ public List getRequests(@Nonnull final UserInfo userInfo) throws NotAuthorizedException {
+ valid(userInfo.getLogin(), userInfo.getPassword(), null);
+ readLockRequest.lock();
+ final Map requestsForUser = requests.get(userInfo.getLogin());
+ readLockRequest.unlock();
+ if (requestsForUser == null) {
+ return new ArrayList<>();
+ }
+ return requestsForUser.entrySet()
+ .stream()
+ .map(entry -> new RequestInfo(entry.getValue().loginFrom,
+ entry.getKey(),
+ entry.getValue().count))
+ .collect(Collectors.toList());
+
+ }
+
+ private String generateKey(@Nonnull final Map map) {
+ Random random = new Random();
+ String newKey;
+ while (true) {
+ char[] chars = "abcdefghijklmnopqrstuvwxyz1234567890".toCharArray();
+ StringBuilder sb = new StringBuilder(20);
+ for (int i = 0; i < 20; i++) {
+ char c = chars[random.nextInt(chars.length)];
+ sb.append(c);
+ }
+ newKey = sb.toString();
+ if (map.get(newKey) == null) {
+ return newKey;
+ }
+ }
+ }
+
+ private boolean transfer(@Nonnull final String loginFrom,
+ @Nonnull final String loginTo,
+ final long count) {
+ writeLockData.lock();
+ final User userFrom = data.get(loginFrom);
+ final User userTo = data.get(loginTo);
+ if (userFrom.count < count) {
+ writeLockData.unlock();
+ return false;
+ }
+ data.put(userFrom.login, new User(userFrom.login,
+ userFrom.password,
+ userFrom.count - count));
+ data.put(userTo.login, new User(userTo.login, userTo.password, userTo.count + count));
+ writeLockData.unlock();
+ return true;
+ }
+
+ private void valid(@Nonnull final String loginFrom,
+ @Nonnull final String password,
+ final String loginTo) throws NotAuthorizedException {
+ readLockData.lock();
+ final User userFrom = data.get(loginFrom);
+ readLockData.unlock();
+ if (userFrom == null) {
+
+ throw new NotAuthorizedException("user " + loginFrom + " not registered");
+ }
+
+ if (loginTo != null && data.get(loginTo) == null) {
+ throw new NotAuthorizedException("user " + loginTo + " not registered");
+ }
+ if (!userFrom.password.equals(password)) {
+
+ throw new NotAuthorizedException("wrong password for " + loginFrom);
+ }
+ }
+
+ private class User {
+ private final String login;
+ private final String password;
+ private final long count;
+
+ private User(@Nonnull final String login, @Nonnull final String password, long count) {
+ this.login = login;
+ this.password = password;
+ this.count = count;
+ }
+
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(login, password, count);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof User)) {
+ return false;
+ }
+ final User that = (User) o;
+ return Objects.equals(this.login, that.login)
+ && Objects.equals(this.password, that.password)
+ && this.count == that.count;
+ }
+ }
+
+
+ private class Request {
+ private final String loginFrom;
+ private final long count;
+
+ private Request(@Nonnull final String loginFrom, long count) {
+ this.loginFrom = loginFrom;
+ this.count = count;
+ }
+
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(loginFrom, count);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Request)) {
+ return false;
+ }
+ final Request that = (Request) o;
+ return Objects.equals(this.loginFrom, that.loginFrom)
+ && this.count == that.count;
+ }
+ }
+}