Skip to content

Commit def4a5d

Browse files
committed
Refactoring of ClientHandler
1 parent 76eda82 commit def4a5d

File tree

13 files changed

+376
-286
lines changed

13 files changed

+376
-286
lines changed

server/src/main/java/com/exactpro/blockchain/CustomerCreditTransferConverter.java

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,49 +2,76 @@
22

33
import com.exactpro.blockchain.entity.Client;
44
import com.exactpro.blockchain.entity.Transfer;
5-
import com.exactpro.blockchain.entity.TransferDetails;
5+
import com.exactpro.blockchain.entity.TransferRequest;
66
import com.exactpro.blockchain.entity.TransferStatus;
77
import com.exactpro.iso20022.CustomerCreditTransfer;
88
import com.exactpro.iso20022.GroupHeader;
99
import com.exactpro.iso20022.Participant;
1010
import com.exactpro.iso20022.TransactionInfo;
11+
import org.springframework.beans.factory.annotation.Value;
12+
import org.springframework.lang.NonNull;
1113
import org.springframework.stereotype.Component;
1214

1315
import java.time.Instant;
14-
import java.time.LocalDate;
16+
import java.time.ZoneId;
1517
import java.util.Collections;
1618
import java.util.List;
1719
import java.util.stream.Collectors;
1820

1921
@Component
2022
public class CustomerCreditTransferConverter {
23+
private final IdGenerator idGenerator = new IdGenerator();
2124

22-
public CustomerCreditTransfer convertFromClientAndTransferDetails(Client client, String clientBic, TransferDetails transferDetails) {
25+
@Value("${client.bic}")
26+
private String myBic;
27+
28+
public Transfer newTransfer(@NonNull Client client, @NonNull TransferRequest transferRequest) {
29+
Instant now = Instant.now();
30+
Transfer.Builder transferBuilder = Transfer.builder();
31+
transferBuilder
32+
.amount(transferRequest.getAmount())
33+
.clientId(client.getClientId())
34+
.creditorBic(transferRequest.getCreditorBic())
35+
.creditorFullName(client.getFullName())
36+
.creditorIban(transferRequest.getCreditorIban())
37+
.currencyCode(transferRequest.getCurrencyCode())
38+
.debtorBic(myBic)
39+
.debtorFullName(client.getFullName())
40+
.debtorIban(transferRequest.getDebtorIban())
41+
.endToEndId(idGenerator.generateId(16))
42+
.remittanceInfo(transferRequest.getComment())
43+
.settlementDate(now.atZone(ZoneId.systemDefault()).toLocalDate())
44+
.status(TransferStatus.PENDING)
45+
.transferTimestamp(now);
46+
return transferBuilder.build();
47+
}
48+
49+
public CustomerCreditTransfer toCustomerCreditTransfer(@NonNull Transfer transfer) {
2350
GroupHeader groupHeader = GroupHeader.builder()
24-
.messageId(IdGenerator.generateId(16))
25-
.timestamp(Instant.now())
51+
.messageId(idGenerator.generateId(16))
52+
.timestamp(transfer.getTransferTimestamp())
2653
.build();
2754

2855
Participant debtor = Participant.builder()
29-
.fullName(client.getFullName())
30-
.iban(transferDetails.getDebtorIban()) // TODO Check that account belongs to the client
31-
.bic(clientBic)
56+
.fullName(transfer.getDebtorFullName())
57+
.iban(transfer.getDebtorIban()) // TODO Check that account belongs to the client
58+
.bic(transfer.getDebtorBic())
3259
.build();
3360

3461
Participant creditor = Participant.builder()
35-
.fullName(transferDetails.getCreditorFullName())
36-
.iban(transferDetails.getCreditorIban())
37-
.bic(transferDetails.getCreditorBic())
62+
.fullName(transfer.getCreditorFullName())
63+
.iban(transfer.getCreditorIban())
64+
.bic(transfer.getCreditorBic())
3865
.build();
3966

4067
TransactionInfo transactionInfo = TransactionInfo.builder()
41-
.endToEndId(IdGenerator.generateId(16))
42-
.currency(transferDetails.getCurrencyCode())
43-
.amount(transferDetails.getAmount())
44-
.settlementDate(LocalDate.now())
68+
.endToEndId(transfer.getEndToEndId())
69+
.currency(transfer.getCurrencyCode())
70+
.amount(transfer.getAmount())
71+
.settlementDate(transfer.getSettlementDate())
4572
.debtor(debtor)
4673
.creditor(creditor)
47-
.remittanceInfo("Transfer")
74+
.remittanceInfo(transfer.getRemittanceInfo())
4875
.build();
4976

5077
return new CustomerCreditTransfer(
@@ -68,10 +95,9 @@ public List<Transfer> convertToTransfer(CustomerCreditTransfer customerCreditTra
6895

6996
return Transfer.builder()
7097
.status(status)
71-
.messageId(messageId)
7298
.transferTimestamp(transferTimestamp)
7399
.endToEndId(transactionInfo.getEndToEndId())
74-
.currency(transactionInfo.getCurrency())
100+
.currencyCode(transactionInfo.getCurrency())
75101
.amount(transactionInfo.getAmount())
76102
.settlementDate(transactionInfo.getSettlementDate())
77103
.debtorFullName(debtor.getFullName())

server/src/main/java/com/exactpro/blockchain/IdGenerator.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
public class IdGenerator {
66
private static final String CHARACTERS = "abcdefghijklmnopqrstuvwxyz0123456789";
77

8-
private static final Random RANDOM = new Random();
8+
private final Random random = new Random();
99

10-
public static String generateId(int length) {
10+
public String generateId(int length) {
1111
StringBuilder sb = new StringBuilder(length);
1212
for (int i = 0; i < length; i++) {
13-
int index = RANDOM.nextInt(CHARACTERS.length());
13+
int index = random.nextInt(CHARACTERS.length());
1414
sb.append(CHARACTERS.charAt(index));
1515
}
1616
return sb.toString();
Lines changed: 117 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,16 @@
11
package com.exactpro.blockchain.api.client;
22

33
import com.exactpro.blockchain.CustomerCreditTransferConverter;
4-
import com.exactpro.blockchain.entity.Account;
5-
import com.exactpro.blockchain.entity.Client;
6-
import com.exactpro.blockchain.entity.Transfer;
7-
import com.exactpro.blockchain.entity.TransferDetails;
8-
import com.exactpro.blockchain.entity.TransferStatus;
4+
import com.exactpro.blockchain.entity.*;
95
import com.exactpro.blockchain.kafka.KafkaPublisher;
10-
import com.exactpro.blockchain.repository.AccountRepository;
11-
import com.exactpro.blockchain.repository.ClientRepository;
12-
import com.exactpro.blockchain.repository.ConversionRateRepository;
13-
import com.exactpro.blockchain.repository.TransferRepository;
6+
import com.exactpro.blockchain.repository.*;
147
import com.exactpro.iso20022.CustomerCreditTransfer;
158
import com.exactpro.iso20022.XmlCodec;
169
import jakarta.xml.bind.JAXBException;
1710
import org.apache.logging.log4j.LogManager;
1811
import org.apache.logging.log4j.Logger;
1912
import org.springframework.beans.factory.annotation.Value;
13+
import org.springframework.lang.NonNull;
2014
import org.springframework.stereotype.Component;
2115
import org.springframework.web.reactive.function.server.ServerRequest;
2216
import org.springframework.web.reactive.function.server.ServerResponse;
@@ -32,28 +26,35 @@ public class ClientHandler {
3226

3327
@Value("${client.bic}")
3428
private String clientBic;
29+
3530
@Value("${default.user}")
3631
private Integer clientId;
32+
3733
private final AccountRepository accountRepository;
3834
private final ClientRepository clientRepository;
3935
private final ConversionRateRepository conversionRateRepository;
40-
private final XmlCodec xmlCodec;
36+
private final CurrencyRepository currencyRepository;
4137
private final TransferRepository transferRepository;
38+
private final XmlCodec xmlCodec;
4239
private final CustomerCreditTransferConverter converter;
4340
private final KafkaPublisher kafkaPublisher;
4441

45-
public ClientHandler(AccountRepository accountRepository,
46-
ClientRepository clientRepository,
47-
ConversionRateRepository conversionRateRepository,
48-
XmlCodec xmlCodec,
49-
TransferRepository transferRepository,
50-
CustomerCreditTransferConverter converter,
51-
KafkaPublisher kafkaPublisher) {
42+
public ClientHandler(
43+
AccountRepository accountRepository,
44+
ClientRepository clientRepository,
45+
CurrencyRepository currencyRepository,
46+
ConversionRateRepository conversionRateRepository,
47+
TransferRepository transferRepository,
48+
XmlCodec xmlCodec,
49+
CustomerCreditTransferConverter converter,
50+
KafkaPublisher kafkaPublisher
51+
) {
5252
this.accountRepository = accountRepository;
5353
this.clientRepository = clientRepository;
5454
this.conversionRateRepository = conversionRateRepository;
55-
this.xmlCodec = xmlCodec;
5655
this.transferRepository = transferRepository;
56+
this.currencyRepository = currencyRepository;
57+
this.xmlCodec = xmlCodec;
5758
this.converter = converter;
5859
this.kafkaPublisher = kafkaPublisher;
5960
}
@@ -67,87 +68,60 @@ public Mono<ServerResponse> getTransfersByClientId(ServerRequest request) {
6768
}
6869

6970
public Mono<ServerResponse> transfer(ServerRequest request) {
70-
return Mono.zip(request.bodyToMono(TransferDetails.class),
71-
clientRepository.findByClientId(clientId).singleOrEmpty())
72-
.flatMap(data -> {
73-
74-
TransferDetails transferDetails = data.getT1();
75-
Client client = data.getT2();
76-
77-
return subtractSelfBalanceMono(clientId, transferDetails)
78-
.flatMap(debitedAccount -> {
79-
80-
CustomerCreditTransfer customerCreditTransfer = converter.convertFromClientAndTransferDetails(client, clientBic, transferDetails);
81-
82-
Transfer transferToSave;
83-
try {
84-
transferToSave = converter.convertToTransfer(customerCreditTransfer, TransferStatus.PENDING).get(0);
85-
} catch (IllegalArgumentException e) {
86-
return Mono.error(new RuntimeException("Failed to convert CustomerCreditTransfer to Transfer", e));
87-
}
88-
89-
return transferRepository.save(transferToSave)
90-
.flatMap(transfer -> {
91-
String encodedTransfer;
92-
try {
93-
encodedTransfer = xmlCodec.encode(customerCreditTransfer);
94-
} catch (JAXBException | TransformerException e) {
95-
return Mono.error(new RuntimeException("Failed to encode to XML", e));
96-
}
97-
98-
return kafkaPublisher.publishMessage(transferDetails.getCreditorBic(), encodedTransfer)
99-
.flatMap(senderResult -> {
100-
transfer.setStatus(TransferStatus.COMPLETED);
101-
return transferRepository.save(transfer);
102-
})
103-
.onErrorResume(e -> {
104-
transfer.setStatus(TransferStatus.FAILED);
105-
return transferRepository.save(transfer)
106-
.flatMap(failedTransfer -> Mono.error(new RuntimeException("Kafka send failed", e)));
107-
})
108-
.then(ServerResponse.accepted()
109-
.bodyValue(MessageFormat.format("Transfer successful for client {0}", clientId)));
110-
});
111-
});
112-
})
113-
.onErrorResume(RuntimeException.class, e -> {
114-
logger.error(e.getMessage(), e);
115-
return ServerResponse.status(500).bodyValue("Internal Server Error");
116-
})
117-
.switchIfEmpty(ServerResponse.badRequest().bodyValue("Invalid request"));
71+
return handleTransfer(request, (transferRequest, client, account, currency) ->
72+
saveTransferRequest(client, transferRequest)
73+
.flatMap(transfer ->
74+
debitAccount(account, transferRequest)
75+
.then(Mono.defer(() -> publishPacs008(transfer)))
76+
.then(Mono.defer(() -> transferRepository.save(transfer.withStatus(TransferStatus.COMPLETED))))
77+
.onErrorResume(ex ->
78+
transferRepository.save(transfer.withStatus(TransferStatus.FAILED))
79+
.flatMap(failedTransfer -> Mono.error(new Exception("Kafka send failed", ex)))
80+
)
81+
.then()
82+
)
83+
);
11884
}
11985

120-
private Mono<Account> subtractSelfBalanceMono(int clientId, TransferDetails transferDetails) {
121-
return accountRepository.findByClientIdAndIban(clientId, transferDetails.getDebtorIban())
122-
.singleOrEmpty()
123-
.switchIfEmpty(Mono.error(
124-
new IllegalArgumentException(MessageFormat.format(
125-
"Debtor Account not found for client ID {0} and IBAN: {1}", clientId, transferDetails.getDebtorIban()))))
126-
.flatMap(account -> {
127-
BigDecimal amountToDebit = transferDetails.getAmount();
128-
String transferCurrency = transferDetails.getCurrencyCode();
129-
String accountCurrency = account.getCurrencyCode();
130-
131-
if (!accountCurrency.equals(transferCurrency)) {
132-
return conversionRateRepository.findByBaseCurrencyAndTargetCurrency(transferCurrency, accountCurrency)
133-
.singleOrEmpty()
134-
.switchIfEmpty(Mono.error(new IllegalArgumentException(MessageFormat.format(
135-
"Conversion rate not found from {0} to {1}", transferCurrency, accountCurrency))))
136-
.flatMap(rate -> {
137-
BigDecimal convertedAmount = amountToDebit.multiply(rate.getRate());
138-
logger.info("Converted amount for debit: {} {} (from {} {}) using rate {}",
139-
convertedAmount, accountCurrency, amountToDebit, transferCurrency, rate.getRate());
140-
return performDebit(account, convertedAmount);
141-
});
142-
} else {
143-
return performDebit(account, amountToDebit);
144-
}
145-
})
146-
.doOnSuccess(debitedAccount -> logger.info("Balance successfully debited for client ID {} (IBAN: {}). New balance: {}. Amount: {}",
147-
clientId, debitedAccount.getIban(), debitedAccount.getBalance(), transferDetails.getAmount()));
86+
private @NonNull Mono<Void> debitAccount(@NonNull Account account, @NonNull TransferRequest transferRequest) {
87+
BigDecimal transferAmount = transferRequest.getAmount();
88+
String transferCurrency = transferRequest.getCurrencyCode();
89+
String accountCurrency = account.getCurrencyCode();
90+
91+
Mono<BigDecimal> amountMono;
92+
if (accountCurrency.equals(transferCurrency)) {
93+
amountMono = Mono.just(transferAmount);
94+
} else {
95+
amountMono =
96+
conversionRateRepository.findByBaseCurrencyAndTargetCurrency(transferCurrency, accountCurrency)
97+
.singleOrEmpty()
98+
.switchIfEmpty(
99+
Mono.error(new Exception(
100+
MessageFormat.format(
101+
"Conversion rate not found from {0} to {1}",
102+
transferCurrency, accountCurrency
103+
)
104+
))
105+
)
106+
.map(rate -> {
107+
BigDecimal convertedAmount = transferAmount.multiply(rate.getRate());
108+
logger.info("Converted amount for debit: {} {} (from {} {}) using rate {}",
109+
convertedAmount, accountCurrency, transferAmount, transferCurrency, rate.getRate());
110+
return convertedAmount;
111+
});
112+
}
113+
return
114+
amountMono
115+
.flatMap(amount -> debitAccount(account, amount))
116+
.doOnSuccess(debitedAccount ->
117+
logger.info("Balance successfully debited for client ID {} (IBAN: {}). New balance: {}. Amount: {}",
118+
clientId, debitedAccount.getIban(), debitedAccount.getBalance(), transferRequest.getAmount()
119+
)
120+
)
121+
.then();
148122
}
149123

150-
private Mono<Account> performDebit(Account account, BigDecimal amountToDebit) {
124+
private @NonNull Mono<Account> debitAccount(@NonNull Account account, @NonNull BigDecimal amountToDebit) {
151125
if (account.getBalance().compareTo(amountToDebit) < 0) {
152126
return Mono.error(new IllegalArgumentException(MessageFormat.format(
153127
"Insufficient funds for account {0}. Current balance: {1}, requested: {2}",
@@ -157,4 +131,52 @@ private Mono<Account> performDebit(Account account, BigDecimal amountToDebit) {
157131
account.setBalance(account.getBalance().subtract(amountToDebit));
158132
return accountRepository.save(account);
159133
}
134+
135+
@FunctionalInterface
136+
private interface TransferHandler {
137+
Mono<Void> handle(
138+
@NonNull TransferRequest transferRequest,
139+
@NonNull Client client,
140+
@NonNull Account account,
141+
@NonNull Currency currency
142+
);
143+
}
144+
145+
private @NonNull Mono<ServerResponse> handleTransfer(ServerRequest request, @NonNull TransferHandler handler) {
146+
return
147+
request.bodyToMono(TransferRequest.class)
148+
.flatMap(transferRequest ->
149+
Mono.zip(
150+
Mono.just(transferRequest),
151+
clientRepository.getByClientId(clientId),
152+
accountRepository.getByClientIdAndIban(clientId, transferRequest.getDebtorIban()),
153+
currencyRepository.getByCurrencyCode(transferRequest.getCurrencyCode())
154+
)
155+
)
156+
.flatMap(data -> handler.handle(data.getT1(), data.getT2(), data.getT3(), data.getT4()))
157+
.then(
158+
ServerResponse.accepted().bodyValue(String.format("Transfer successful for client %d", clientId))
159+
)
160+
.onErrorResume(RuntimeException.class, e -> {
161+
logger.error(e.getMessage(), e);
162+
return ServerResponse.status(500).bodyValue("Internal Server Error");
163+
});
164+
}
165+
166+
private @NonNull Mono<Transfer> saveTransferRequest(
167+
@NonNull Client client, @NonNull TransferRequest transferRequest
168+
) {
169+
return transferRepository.save(converter.newTransfer(client, transferRequest));
170+
}
171+
172+
private @NonNull Mono<Void> publishPacs008(@NonNull Transfer transfer) {
173+
CustomerCreditTransfer customerCreditTransfer = converter.toCustomerCreditTransfer(transfer);
174+
String pacs008XmlString;
175+
try {
176+
pacs008XmlString = xmlCodec.encode(customerCreditTransfer);
177+
} catch (JAXBException | TransformerException ex) {
178+
return Mono.error(new Exception("Failed to encode transfer to pacs.008 XML", ex));
179+
}
180+
return kafkaPublisher.publishMessage(transfer.getCreditorBic(), pacs008XmlString);
181+
}
160182
}

server/src/main/java/com/exactpro/blockchain/api/client/ClientRouter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public class ClientRouter {
1919
public @NonNull RouterFunction<ServerResponse> clientRoutes(@NonNull ClientHandler clientHandler) {
2020
return route(GET(contentPath + "/api/client/account"), clientHandler::getAccountsByClientId)
2121
.andRoute(GET(contentPath + "/api/client/transfer"), clientHandler::getTransfersByClientId)
22-
.andRoute(POST(contentPath + "/api/client/makeTransfer"), clientHandler::transfer);
22+
.andRoute(POST(contentPath + "/api/client/transfer"), clientHandler::transfer);
2323
}
2424
}

0 commit comments

Comments
 (0)