11package com .exactpro .blockchain .api .client ;
22
33import 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 .*;
95import 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 .*;
147import com .exactpro .iso20022 .CustomerCreditTransfer ;
158import com .exactpro .iso20022 .XmlCodec ;
169import jakarta .xml .bind .JAXBException ;
1710import org .apache .logging .log4j .LogManager ;
1811import org .apache .logging .log4j .Logger ;
1912import org .springframework .beans .factory .annotation .Value ;
13+ import org .springframework .lang .NonNull ;
2014import org .springframework .stereotype .Component ;
2115import org .springframework .web .reactive .function .server .ServerRequest ;
2216import 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}
0 commit comments