From a66db7d7e4a5e1fd5bf54bf79692f93e15da193b Mon Sep 17 00:00:00 2001 From: alexk11 Date: Fri, 30 Jan 2026 22:50:41 +0300 Subject: [PATCH 01/12] added rabbitmq --- .../docker-compose.yml => docker-compose.yml | 22 ++++++++++++++++-- rabbitmq_delayed_message_exchange-4.2.0.ez | Bin 0 -> 42795 bytes xpayment-adapter-app/pom.xml | 4 ++++ .../main/resources/application-rabbitmq.yaml | 14 +++++++++++ .../src/main/resources/application.yaml | 2 +- 5 files changed, 39 insertions(+), 3 deletions(-) rename payment-service-app/docker-compose.yml => docker-compose.yml (79%) create mode 100644 rabbitmq_delayed_message_exchange-4.2.0.ez create mode 100644 xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml diff --git a/payment-service-app/docker-compose.yml b/docker-compose.yml similarity index 79% rename from payment-service-app/docker-compose.yml rename to docker-compose.yml index 0b616b6..316411a 100644 --- a/payment-service-app/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,8 @@ services: payment-service-app: image: payment-service-app build: - context: . - dockerfile: Dockerfile + context: payment-service-app + dockerfile: payment-service-app/Dockerfile ports: - "8082:8082" depends_on: @@ -98,6 +98,24 @@ services: depends_on: - kafka + rabbitmq: + image: rabbitmq:4.1.4-management + container_name: rabbitmq + ports: + - "5672:5672" # для подключения клиентов к брокеру + - "15672:15672" # для доступа к админке + environment: + RABBITMQ_DEFAULT_USER: admin + RABBITMQ_DEFAULT_PASS: admin + volumes: + - rabbitmq_data:/var/lib/rabbitmq + # делаем доступным в контейнере файл плагина + - ./rabbitmq_delayed_message_exchange-4.2.0.ez:/opt/rabbitmq/plugins/rabbitmq_delayed_message_exchange-4.2.0.ez + command: > + bash -c "rabbitmq-plugins enable --offline rabbitmq_delayed_message_exchange && rabbitmq-server" + restart: unless-stopped + volumes: pgdata: kafka_data: + rabbitmq_data: \ No newline at end of file diff --git a/rabbitmq_delayed_message_exchange-4.2.0.ez b/rabbitmq_delayed_message_exchange-4.2.0.ez new file mode 100644 index 0000000000000000000000000000000000000000..8ba5d78cb45d3e6cae5ee747fb7f88089912a98a GIT binary patch literal 42795 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%0AF?%B5zj{;h@Te@pj_Y_pn0MY8utTj&guo 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 Sg@AuMbfF<2+VcK#_kRG9DzP{K literal 0 HcmV?d00001 diff --git a/xpayment-adapter-app/pom.xml b/xpayment-adapter-app/pom.xml index 1e05034..0aa1412 100644 --- a/xpayment-adapter-app/pom.xml +++ b/xpayment-adapter-app/pom.xml @@ -44,6 +44,10 @@ org.springframework.kafka spring-kafka + + org.springframework.boot + spring-boot-starter-amqp + com.iprody payment-service-api diff --git a/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml new file mode 100644 index 0000000..f976bf7 --- /dev/null +++ b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml @@ -0,0 +1,14 @@ +spring: + + rabbitmq: + host: localhost + port: 5672 + username: admin + password: admin + + app: + rabbitmq: + exchange-name: payment-state-check-exchange + queue-name: payment-state-check-queue + max-retries: 60 + interval-ms: 60000 \ No newline at end of file diff --git a/xpayment-adapter-app/src/main/resources/application.yaml b/xpayment-adapter-app/src/main/resources/application.yaml index 303d059..9cc777e 100644 --- a/xpayment-adapter-app/src/main/resources/application.yaml +++ b/xpayment-adapter-app/src/main/resources/application.yaml @@ -6,7 +6,7 @@ spring: application: name: xpayment-adapter-app profiles: - active: kafka + active: kafka, rabbitmq security: oauth2: resourceserver: From a10bd17cce2e86b04b83997e07d574d9fcd3d2ee Mon Sep 17 00:00:00 2001 From: alexk11 Date: Sun, 1 Feb 2026 00:09:53 +0300 Subject: [PATCH 02/12] added rabbitmq --- docker-compose.yml | 2 +- .../src/main/resources/application-db.yaml | 4 +- .../src/main/resources/application.yaml | 16 ++-- pom.xml | 1 + xpayment-adapter-app/pom.xml | 7 ++ .../async/handler/RequestMessageHandler.java | 52 ++++++----- .../checkstate/PaymentCheckStateMessage.java | 58 +++++++++++++ .../checkstate/PaymentStateCheckListener.java | 87 +++++++++++++++++++ .../PaymentStateCheckRegistrar.java | 12 +++ .../PaymentStateCheckRegistrarImpl.java | 60 +++++++++++++ .../adapter/checkstate/RabbitMqDlxConfig.java | 27 ++++++ .../RabbitMqPaymentRetryConfig.java | 47 ++++++++++ .../handler/PaymentStatusCheckHandler.java | 17 ++++ .../adapter/dto/CreateChargeRequestDto.java | 3 + .../adapter/dto/CreateChargeResponseDto.java | 3 + 15 files changed, 365 insertions(+), 31 deletions(-) create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentCheckStateMessage.java create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckListener.java create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrar.java create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqDlxConfig.java create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/handler/PaymentStatusCheckHandler.java diff --git a/docker-compose.yml b/docker-compose.yml index 316411a..d3aac1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -99,7 +99,7 @@ services: - kafka rabbitmq: - image: rabbitmq:4.1.4-management + image: rabbitmq:4.2.3-management-alpine container_name: rabbitmq ports: - "5672:5672" # для подключения клиентов к брокеру diff --git a/payment-service-app/src/main/resources/application-db.yaml b/payment-service-app/src/main/resources/application-db.yaml index 32db3a1..e916081 100644 --- a/payment-service-app/src/main/resources/application-db.yaml +++ b/payment-service-app/src/main/resources/application-db.yaml @@ -1,8 +1,8 @@ spring: datasource: - url: jdbc:postgresql://localhost:5432/payment-db # URL подключения к БД - #url: jdbc:postgresql://postgres-db:5432/payment-db + #url: jdbc:postgresql://localhost:5432/payment-db # URL подключения к БД + url: jdbc:postgresql://postgres-db:5432/payment-db username: user # Имя пользователя password: secret # Пароль driver-class-name: org.postgresql.Driver # класс jdbc драйвера diff --git a/payment-service-app/src/main/resources/application.yaml b/payment-service-app/src/main/resources/application.yaml index 11ad695..88a0448 100644 --- a/payment-service-app/src/main/resources/application.yaml +++ b/payment-service-app/src/main/resources/application.yaml @@ -13,14 +13,14 @@ spring: jwt: issuer-uri: http://localhost:8085/realms/iprody-lms -# logging: -# level: -# root: INFO -# org.hibernate: ERROR -# # com.iprody.payment: ERROR -# org.springframework.web: WARN -# org.springframework.data: WARN -# org.springframework.security: WARN + logging: + level: + root: INFO + org.hibernate: ERROR + com.iprody: INFO + org.springframework.web: WARN + org.springframework.data: WARN + org.springframework.security: WARN management: endpoints: diff --git a/pom.xml b/pom.xml index 5847f9c..5540bbf 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ 3.6.0 1.6.3 1.21.4 + 3.5.8 0.0.1-SNAPSHOT diff --git a/xpayment-adapter-app/pom.xml b/xpayment-adapter-app/pom.xml index 0aa1412..40fb04d 100644 --- a/xpayment-adapter-app/pom.xml +++ b/xpayment-adapter-app/pom.xml @@ -47,6 +47,7 @@ org.springframework.boot spring-boot-starter-amqp + ${amqp.version} com.iprody @@ -59,6 +60,12 @@ ${lombok.version} true + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.21.0 + compile + org.openapitools jackson-databind-nullable diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java index 43be2a4..857c963 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java @@ -1,13 +1,15 @@ package com.iprody.adapter.async.handler; -import com.iprody.adapter.api.XPaymentProviderGateway; +import com.iprody.adapter.dto.CreateChargeRequestDto; +import com.iprody.adapter.dto.CreateChargeResponseDto; +import com.iprody.adapter.mapper.XPaymentMapper; import com.iprody.api.AsyncSender; import com.iprody.api.XPaymentAdapterStatus; import com.iprody.api.dto.XPaymentAdapterRequestMessage; import com.iprody.api.dto.XPaymentAdapterResponseMessage; +import com.iprody.adapter.api.XPaymentProviderGateway; +import com.iprody.adapter.checkstate.PaymentStateCheckRegistrar; -import com.iprody.xpayment.app.api.model.ChargeResponse; -import com.iprody.xpayment.app.api.model.CreateChargeRequest; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -16,39 +18,46 @@ import java.time.OffsetDateTime; -@Slf4j @Component -public class RequestMessageHandler implements MessageHandler { +@Slf4j +public class RequestMessageHandler implements + MessageHandler { private final XPaymentProviderGateway xPaymentProviderGateway; private final AsyncSender asyncSender; + private final PaymentStateCheckRegistrar paymentStateCheckRegistrar; + private final XPaymentMapper mapper; @Autowired public RequestMessageHandler( XPaymentProviderGateway xPaymentProviderGateway, - AsyncSender asyncSender) { + AsyncSender asyncSender, + PaymentStateCheckRegistrar paymentStateCheckRegistrar, + XPaymentMapper mapper) { this.xPaymentProviderGateway = xPaymentProviderGateway; this.asyncSender = asyncSender; + this.paymentStateCheckRegistrar = paymentStateCheckRegistrar; + this.mapper = mapper; } @Override public void handle(XPaymentAdapterRequestMessage message) { - log.info("Payment request received paymentGuid - {}, amount - {}, currency - {}", + message.getPaymentGuid(), + message.getAmount(), + message.getCurrency()); - message.getPaymentGuid(), message.getAmount(), message.getCurrency()); - - CreateChargeRequest createChargeRequest = new CreateChargeRequest(); - createChargeRequest.setAmount(message.getAmount()); - createChargeRequest.setCurrency(message.getCurrency()); - createChargeRequest.setOrder(message.getPaymentGuid()); + CreateChargeRequestDto dto = new CreateChargeRequestDto(); + dto.setAmount(message.getAmount()); + dto.setCurrency(message.getCurrency()); + dto.setOrder(message.getPaymentGuid()); try { - ChargeResponse chargeResponse = - xPaymentProviderGateway.createCharge(createChargeRequest); + CreateChargeResponseDto chargeResponse = + xPaymentProviderGateway.createCharge(dto); log.info("Payment request with paymentGuid - {} is sent for payment processing. " + - "Current status - ", chargeResponse.getStatus()); + "Current status - ", chargeResponse.getStatus()); XPaymentAdapterResponseMessage responseMessage = new XPaymentAdapterResponseMessage(); responseMessage.setPaymentGuid(chargeResponse.getOrder()); @@ -56,15 +65,18 @@ public void handle(XPaymentAdapterRequestMessage message) { responseMessage.setAmount(chargeResponse.getAmount()); responseMessage.setCurrency(chargeResponse.getCurrency()); responseMessage.setStatus(XPaymentAdapterStatus.valueOf(chargeResponse.getStatus())); - responseMessage.setOccurredAt(OffsetDateTime.now()); - asyncSender.send(responseMessage); + asyncSender.send(responseMessage); + paymentStateCheckRegistrar.register( + chargeResponse.getId(), + chargeResponse.getOrder(), + chargeResponse.getAmount(), + chargeResponse.getCurrency() + ); } catch (RestClientException ex) { - log.error("Error in time of sending payment request with paymentGuid - {}", message.getPaymentGuid(), ex); - XPaymentAdapterResponseMessage responseMessage = new XPaymentAdapterResponseMessage(); responseMessage.setPaymentGuid(message.getPaymentGuid()); responseMessage.setAmount(message.getAmount()); diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentCheckStateMessage.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentCheckStateMessage.java new file mode 100644 index 0000000..1de7a4a --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentCheckStateMessage.java @@ -0,0 +1,58 @@ +package com.iprody.adapter.checkstate; + +import java.math.BigDecimal; +import java.util.UUID; + + +public class PaymentCheckStateMessage { + + private UUID chargeGuid; + private UUID paymentGuid; + + private BigDecimal amount; + private String currency; + + public PaymentCheckStateMessage( + UUID chargeGuid, + UUID paymentGuid, + BigDecimal amount, + String currency) { + this.chargeGuid = chargeGuid; + this.paymentGuid = paymentGuid; + this.amount = amount; + this.currency = currency; + } + + public UUID getChargeGuid() { + return chargeGuid; + } + + public void setChargeGuid(UUID chargeGuid) { + this.chargeGuid = chargeGuid; + } + + public UUID getPaymentGuid() { + return paymentGuid; + } + + public void setPaymentGuid(UUID paymentGuid) { + this.paymentGuid = paymentGuid; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getCurrency() { + return currency; + } + + public void setCurrency(String currency) { + this.currency = currency; + } + +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckListener.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckListener.java new file mode 100644 index 0000000..750250b --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckListener.java @@ -0,0 +1,87 @@ +package com.iprody.adapter.checkstate; + +import com.iprody.adapter.checkstate.handler.PaymentStatusCheckHandler; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageProperties; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + + +@Component +public class PaymentStateCheckListener { + + private final RabbitTemplate rabbitTemplate; + private final String exchangeName; + private final String routingKey; + private final String dlxExchangeName; + private final String dlxRoutingKey; + private final PaymentStatusCheckHandler paymentStatusCheckHandler; + + @Value("${app.rabbitmq.max-retries:60}") + private int maxRetries; + + @Value("${app.rabbitmq.interval-ms:60000}") + private long intervalMs; + + @Autowired + public PaymentStateCheckListener( + RabbitTemplate rabbitTemplate, + @Value("${app.rabbitmq.delayed-exchange-name}") String exchangeName, + @Value("${app.rabbitmq.queue-name}") String routingKey, + @Value("${app.rabbitmq.dlx-exchange-name}") String dlxExchangeName, + @Value("${app.rabbitmq.dlx-routing-key}") String dlxRoutingKey, + PaymentStatusCheckHandler paymentStatusCheckHandler) { + this.rabbitTemplate = rabbitTemplate; + this.exchangeName = exchangeName; + this.routingKey = routingKey; + this.dlxExchangeName = dlxExchangeName; + this.dlxRoutingKey = dlxRoutingKey; + this.paymentStatusCheckHandler = paymentStatusCheckHandler; + } + + @RabbitListener(queues = "${app.rabbitmq.queue-name}") + public void handle(PaymentCheckStateMessage message, Message raw) { + MessageProperties props = raw.getMessageProperties(); + int retryCount = (int)props.getHeaders().getOrDefault("x-retry-count", 0); + boolean paid = paymentStatusCheckHandler.handle(message.getChargeGuid()); + if (paid) { + return; + } + if (retryCount < maxRetries) { + // Планируем следующую проверку + PaymentCheckStateMessage newMessage = new PaymentCheckStateMessage( + message.getChargeGuid(), + message.getPaymentGuid(), + message.getAmount(), + message.getCurrency() + ); + rabbitTemplate.convertAndSend( + exchangeName, + routingKey, + newMessage, + m -> { + m.getMessageProperties().setHeader("x-delay", intervalMs); + m.getMessageProperties().setHeader("x-retry-count", retryCount + 1); + return m; + } + ); + } else { + // Исчерпали попытки -- кладём сообщение в DLX + rabbitTemplate.convertAndSend( + dlxExchangeName, + dlxRoutingKey, + message, + m -> { + m.getMessageProperties().setHeader("x-retry-count", retryCount); + m.getMessageProperties().setHeader("x-final-status", "TIMEOUT"); + m.getMessageProperties().setHeader("x-original-queue", props.getConsumerQueue()); + return m; + } + ); + } + } + +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrar.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrar.java new file mode 100644 index 0000000..148ef5e --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrar.java @@ -0,0 +1,12 @@ +package com.iprody.adapter.checkstate; + +import java.math.BigDecimal; +import java.util.UUID; + +public interface PaymentStateCheckRegistrar { + void register( + UUID chargeGuid, + UUID paymentGuid, + BigDecimal amount, + String currency); +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java new file mode 100644 index 0000000..2c66061 --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java @@ -0,0 +1,60 @@ +package com.iprody.adapter.checkstate; + +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.util.UUID; + + +@Service +public class PaymentStateCheckRegistrarImpl implements PaymentStateCheckRegistrar { + + private final RabbitTemplate rabbitTemplate; + private final String exchangeName; + private final String routingKey; + + @Value("${app.rabbitmq.max-retries:60}") + private int maxRetries; + + @Value("${app.rabbitmq.interval-ms:60000}") + private long intervalMs; + + @Autowired + public PaymentStateCheckRegistrarImpl( + RabbitTemplate rabbitTemplate, + @Value("${app.rabbitmq.delayed-exchange-name}") String exchangeName, + @Value("${app.rabbitmq.queue-name}") String routingKey) { + this.rabbitTemplate = rabbitTemplate; + this.exchangeName = exchangeName; + this.routingKey = routingKey; + } + + @Override + public void register( + UUID chargeGuid, + UUID paymentGuid, + BigDecimal amount, + String currency) { + + PaymentCheckStateMessage message = new PaymentCheckStateMessage( + chargeGuid, + paymentGuid, + amount, + currency + ); + + rabbitTemplate.convertAndSend( + exchangeName, + routingKey, + message, + m -> { + m.getMessageProperties().setHeader("x-delay", intervalMs); + m.getMessageProperties().setHeader("x-retry-count", 1); + return m; + } + ); + } + +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqDlxConfig.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqDlxConfig.java new file mode 100644 index 0000000..8fbbe29 --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqDlxConfig.java @@ -0,0 +1,27 @@ +package com.iprody.adapter.checkstate; + +import org.springframework.amqp.core.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class RabbitMqDlxConfig { + + @Bean + DirectExchange deadLetterExchange() { + return new DirectExchange("payments.dlx"); + } + + @Bean + Queue deadLetterQueue() { + return QueueBuilder.durable("payments.dead.queue").build(); + } + + @Bean + Binding dlxBinding() { + return BindingBuilder.bind(deadLetterQueue()) + .to(deadLetterExchange()) + .with("payments.dead"); + } + +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java new file mode 100644 index 0000000..a5db479 --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java @@ -0,0 +1,47 @@ +package com.iprody.adapter.checkstate; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.CustomExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.core.QueueBuilder; + +import java.util.Map; + + +@Configuration +public class RabbitMqPaymentRetryConfig { + + @Value("${app.rabbitmq.queue-name}") + private String queueName; + + @Value("${app.rabbitmq.exchange-name}") + private String delayedExchangeName; + + @Bean + public Queue xpaymentQueue() { + + return QueueBuilder.durable(queueName) + .withArgument("x-dead-letter-exchange", "payments.dlx") + .withArgument("x-dead-letter-routing-key", "payments.dead") + .build(); + } + + @Bean + public CustomExchange delayedExchange() { + return new CustomExchange(delayedExchangeName, + "x-delayed-message", + true, + false, + Map.of("x-delayed-type", "direct")); + } + + @Bean + public Binding queueBinding(Queue xpaymentQueue, CustomExchange delayedExchange) { + return BindingBuilder.bind(xpaymentQueue).to(delayedExchange).with(queueName).noargs(); + } + +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/handler/PaymentStatusCheckHandler.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/handler/PaymentStatusCheckHandler.java new file mode 100644 index 0000000..848859b --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/handler/PaymentStatusCheckHandler.java @@ -0,0 +1,17 @@ +package com.iprody.adapter.checkstate.handler; + +import java.util.UUID; + +public interface PaymentStatusCheckHandler { + /** + * Проверяет статус платежа в X Payment Provider по заданному + идентификатору. Если статус нетерминальный, то метод возвращает + false. В противном случае, отправляет асинхронное уведомление + Payment Service o измененном статус платежа и возвращает true. + * + * @param paymentGuid UUID платежа для проверки + * @return true, если платеж завершен и новые проверки статуса + не требуются, иначе false + */ + boolean handle(UUID paymentGuid); +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeRequestDto.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeRequestDto.java index 2aaedff..fa1c9d6 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeRequestDto.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeRequestDto.java @@ -1,10 +1,13 @@ package com.iprody.adapter.dto; +import lombok.Setter; + import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; import java.util.UUID; +@Setter public class CreateChargeRequestDto { private BigDecimal amount; private String currency; diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeResponseDto.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeResponseDto.java index 9578778..5d4197e 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeResponseDto.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeResponseDto.java @@ -1,10 +1,13 @@ package com.iprody.adapter.dto; +import lombok.Getter; + import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; import java.util.UUID; +@Getter public class CreateChargeResponseDto { private UUID id; private BigDecimal amount; From 5a9f6c400e4c22f65bed1109c91cb57cab52f9bb Mon Sep 17 00:00:00 2001 From: alexk11 Date: Mon, 2 Feb 2026 23:12:35 +0300 Subject: [PATCH 03/12] added rabbitmq --- docker-compose.yml | 5 ++- payment-service-api/pom.xml | 2 +- payment-service-app/pom.xml | 4 +- .../src/main/resources/application-db.yaml | 4 +- pom.xml | 2 +- simple-http-server/pom.xml | 2 +- xpayment-adapter-app/pom.xml | 24 ++++++++++- .../api/XPaymentProviderGatewayImpl.java | 18 ++++++-- .../async/handler/RequestMessageHandler.java | 2 +- .../KafkaXPaymentAdapterMessageProducer.java | 3 +- .../checkstate/PaymentStateCheckListener.java | 14 +++---- .../PaymentStateCheckRegistrarImpl.java | 12 ++++-- .../RabbitMqPaymentRetryConfig.java | 11 ++++- .../PaymentStatusCheckerHandlerImpl.java | 37 +++++++++++++++++ .../adapter/config/KafkaProperties.java | 14 ++++++- .../config/XPaymentRestClientConfig.java | 8 ++-- .../adapter/dto/CreateChargeRequestDto.java | 2 + .../adapter/dto/CreateChargeResponseDto.java | 2 + .../adapter/mapper/XPaymentConverter.java | 41 +++++++++++++++++++ .../iprody/adapter/mapper/XPaymentMapper.java | 24 +++++++++-- .../src/main/resources/application-kafka.yaml | 6 --- .../main/resources/application-rabbitmq.yaml | 3 ++ .../src/main/resources/application.yaml | 9 +++- 23 files changed, 202 insertions(+), 47 deletions(-) create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/handler/PaymentStatusCheckerHandlerImpl.java create mode 100644 xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentConverter.java diff --git a/docker-compose.yml b/docker-compose.yml index d3aac1a..ad4f13c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -57,11 +57,11 @@ services: - --import-realm environment: KEYCLOAK_ADMIN: admin - KEYCLOAK_ADMIN_PASSWORD: admin + KEYCLOAK_ADMIN_PASSWORD: password ports: - "8085:8080" volumes: - - ./src/main/resources/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json + - ./payment-service-app/src/main/resources/keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json restart: unless-stopped kafka: @@ -116,6 +116,7 @@ services: restart: unless-stopped volumes: + postgres_data: pgdata: kafka_data: rabbitmq_data: \ No newline at end of file diff --git a/payment-service-api/pom.xml b/payment-service-api/pom.xml index 6f44ea0..4224b92 100644 --- a/payment-service-api/pom.xml +++ b/payment-service-api/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.iprody - PaymentService + Payment-Service-Main 1.0-SNAPSHOT ../pom.xml diff --git a/payment-service-app/pom.xml b/payment-service-app/pom.xml index 392a548..b4a06e2 100644 --- a/payment-service-app/pom.xml +++ b/payment-service-app/pom.xml @@ -4,14 +4,14 @@ 4.0.0 com.iprody - PaymentService + Payment-Service-Main 1.0-SNAPSHOT ../pom.xml payment-service-app 0.0.1-SNAPSHOT - Payment Service App + payment-service-app Payment Service application jar diff --git a/payment-service-app/src/main/resources/application-db.yaml b/payment-service-app/src/main/resources/application-db.yaml index e916081..32db3a1 100644 --- a/payment-service-app/src/main/resources/application-db.yaml +++ b/payment-service-app/src/main/resources/application-db.yaml @@ -1,8 +1,8 @@ spring: datasource: - #url: jdbc:postgresql://localhost:5432/payment-db # URL подключения к БД - url: jdbc:postgresql://postgres-db:5432/payment-db + url: jdbc:postgresql://localhost:5432/payment-db # URL подключения к БД + #url: jdbc:postgresql://postgres-db:5432/payment-db username: user # Имя пользователя password: secret # Пароль driver-class-name: org.postgresql.Driver # класс jdbc драйвера diff --git a/pom.xml b/pom.xml index 5540bbf..600bd85 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.iprody - PaymentService + Payment-Service-Main 1.0-SNAPSHOT pom diff --git a/simple-http-server/pom.xml b/simple-http-server/pom.xml index dcc36f7..cbf53b2 100644 --- a/simple-http-server/pom.xml +++ b/simple-http-server/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.iprody - PaymentService + Payment-Service-Main 1.0-SNAPSHOT diff --git a/xpayment-adapter-app/pom.xml b/xpayment-adapter-app/pom.xml index 40fb04d..06fdc77 100644 --- a/xpayment-adapter-app/pom.xml +++ b/xpayment-adapter-app/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.iprody - PaymentService + Payment-Service-Main 1.0-SNAPSHOT ../pom.xml @@ -47,7 +47,7 @@ org.springframework.boot spring-boot-starter-amqp - ${amqp.version} + 3.5.8 com.iprody @@ -82,6 +82,26 @@ ${mapstruct.version} provided + + org.junit.jupiter + junit-jupiter-api + 5.10.1 + test + + + org.junit.jupiter + junit-jupiter + 5.10.1 + test + + + junit + junit + + + org.junit.jupiter + junit-jupiter-api + diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/api/XPaymentProviderGatewayImpl.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/api/XPaymentProviderGatewayImpl.java index 652fa18..700da39 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/api/XPaymentProviderGatewayImpl.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/api/XPaymentProviderGatewayImpl.java @@ -2,8 +2,10 @@ import com.iprody.adapter.dto.CreateChargeRequestDto; import com.iprody.adapter.dto.CreateChargeResponseDto; +import com.iprody.adapter.mapper.XPaymentConverter; import com.iprody.adapter.mapper.XPaymentMapper; import com.iprody.xpayment.app.api.client.DefaultApi; +import com.iprody.xpayment.app.api.model.ChargeResponse; import com.iprody.xpayment.app.api.model.CreateChargeRequest; import org.springframework.stereotype.Service; @@ -17,18 +19,25 @@ class XPaymentProviderGatewayImpl implements XPaymentProviderGateway { private final DefaultApi defaultApi; private final XPaymentMapper mapper; + private final XPaymentConverter converter; - public XPaymentProviderGatewayImpl(DefaultApi defaultApi, XPaymentMapper mapper) { + public XPaymentProviderGatewayImpl(DefaultApi defaultApi, + XPaymentMapper mapper, + XPaymentConverter converter) { this.defaultApi = defaultApi; this.mapper = mapper; + this.converter = converter; } @Override public CreateChargeResponseDto createCharge(CreateChargeRequestDto dto) throws RestClientException { try { - CreateChargeRequest chargeRequest = mapper.toCreateChargeRequest(dto); - return mapper.toCreateChargeResponseDto(defaultApi.createCharge(chargeRequest)); + //CreateChargeRequest chargeRequest = mapper.toCreateChargeRequest(dto); + CreateChargeRequest chargeRequest = converter.toCreateChargeRequest(dto); + ChargeResponse response = defaultApi.createCharge(chargeRequest); + //return mapper.toCreateChargeResponseDto(response); + return converter.toCreateChargeResponseDto(response); } catch (Exception e) { throw toRestClientException("POST /charges failed", e); } @@ -37,7 +46,8 @@ public CreateChargeResponseDto createCharge(CreateChargeRequestDto dto) @Override public CreateChargeResponseDto retrieveCharge(UUID id) throws RestClientException { try { - return mapper.toCreateChargeResponseDto(defaultApi.retrieveCharge(id)); + //return mapper.toCreateChargeResponseDto(defaultApi.retrieveCharge(id)); + return converter.toCreateChargeResponseDto(defaultApi.retrieveCharge(id)); } catch (Exception e) { throw toRestClientException("GET /charges/{id} failed (id=" + id + ")", e); } diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java index 857c963..474796c 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java @@ -57,7 +57,7 @@ public void handle(XPaymentAdapterRequestMessage message) { xPaymentProviderGateway.createCharge(dto); log.info("Payment request with paymentGuid - {} is sent for payment processing. " + - "Current status - ", chargeResponse.getStatus()); + "Current status {}", message.getPaymentGuid(), chargeResponse.getStatus()); XPaymentAdapterResponseMessage responseMessage = new XPaymentAdapterResponseMessage(); responseMessage.setPaymentGuid(chargeResponse.getOrder()); diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/kafka/KafkaXPaymentAdapterMessageProducer.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/kafka/KafkaXPaymentAdapterMessageProducer.java index 2258daa..c7addde 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/kafka/KafkaXPaymentAdapterMessageProducer.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/kafka/KafkaXPaymentAdapterMessageProducer.java @@ -20,9 +20,10 @@ public class KafkaXPaymentAdapterMessageProducer implements AsyncSender topic={}", + log.info("Sending XPayment Adapter response: guid={}, amount={}, status={}, currency={} -> topic={}", msg.getPaymentGuid(), msg.getAmount(), + msg.getStatus(), msg.getCurrency(), kafkaProperties.getResponseTopic()); diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckListener.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckListener.java index 750250b..b535394 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckListener.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckListener.java @@ -20,19 +20,19 @@ public class PaymentStateCheckListener { private final String dlxRoutingKey; private final PaymentStatusCheckHandler paymentStatusCheckHandler; - @Value("${app.rabbitmq.max-retries:60}") + @Value("${spring.app.rabbitmq.max-retries:60}") private int maxRetries; - @Value("${app.rabbitmq.interval-ms:60000}") + @Value("${spring.app.rabbitmq.interval-ms:60000}") private long intervalMs; @Autowired public PaymentStateCheckListener( RabbitTemplate rabbitTemplate, - @Value("${app.rabbitmq.delayed-exchange-name}") String exchangeName, - @Value("${app.rabbitmq.queue-name}") String routingKey, - @Value("${app.rabbitmq.dlx-exchange-name}") String dlxExchangeName, - @Value("${app.rabbitmq.dlx-routing-key}") String dlxRoutingKey, + @Value("${spring.app.rabbitmq.delayed-exchange-name}") String exchangeName, + @Value("${spring.app.rabbitmq.queue-name}") String routingKey, + @Value("${spring.app.rabbitmq.dlx-exchange-name}") String dlxExchangeName, + @Value("${spring.app.rabbitmq.dlx-routing-key}") String dlxRoutingKey, PaymentStatusCheckHandler paymentStatusCheckHandler) { this.rabbitTemplate = rabbitTemplate; this.exchangeName = exchangeName; @@ -42,7 +42,7 @@ public PaymentStateCheckListener( this.paymentStatusCheckHandler = paymentStatusCheckHandler; } - @RabbitListener(queues = "${app.rabbitmq.queue-name}") + @RabbitListener(queues = "${spring.app.rabbitmq.queue-name}") public void handle(PaymentCheckStateMessage message, Message raw) { MessageProperties props = raw.getMessageProperties(); int retryCount = (int)props.getHeaders().getOrDefault("x-retry-count", 0); diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java index 2c66061..432813e 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java @@ -1,5 +1,6 @@ package com.iprody.adapter.checkstate; +import lombok.extern.slf4j.Slf4j; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -9,23 +10,24 @@ @Service +@Slf4j public class PaymentStateCheckRegistrarImpl implements PaymentStateCheckRegistrar { private final RabbitTemplate rabbitTemplate; private final String exchangeName; private final String routingKey; - @Value("${app.rabbitmq.max-retries:60}") + @Value("${spring.app.rabbitmq.max-retries:60}") private int maxRetries; - @Value("${app.rabbitmq.interval-ms:60000}") + @Value("${spring.app.rabbitmq.interval-ms:60000}") private long intervalMs; @Autowired public PaymentStateCheckRegistrarImpl( RabbitTemplate rabbitTemplate, - @Value("${app.rabbitmq.delayed-exchange-name}") String exchangeName, - @Value("${app.rabbitmq.queue-name}") String routingKey) { + @Value("${spring.app.rabbitmq.delayed-exchange-name}") String exchangeName, + @Value("${spring.app.rabbitmq.queue-name}") String routingKey) { this.rabbitTemplate = rabbitTemplate; this.exchangeName = exchangeName; this.routingKey = routingKey; @@ -38,6 +40,8 @@ public void register( BigDecimal amount, String currency) { + log.info("Registering message with RabbitMQ, id={}", paymentGuid); + PaymentCheckStateMessage message = new PaymentCheckStateMessage( chargeGuid, paymentGuid, diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java index a5db479..59bebd8 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java @@ -1,5 +1,7 @@ package com.iprody.adapter.checkstate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -15,10 +17,10 @@ @Configuration public class RabbitMqPaymentRetryConfig { - @Value("${app.rabbitmq.queue-name}") + @Value("${spring.app.rabbitmq.queue-name}") private String queueName; - @Value("${app.rabbitmq.exchange-name}") + @Value("${spring.app.rabbitmq.exchange-name}") private String delayedExchangeName; @Bean @@ -44,4 +46,9 @@ public Binding queueBinding(Queue xpaymentQueue, CustomExchange delayedExchange) return BindingBuilder.bind(xpaymentQueue).to(delayedExchange).with(queueName).noargs(); } + @Bean + public MessageConverter messageConverter() { + return new Jackson2JsonMessageConverter(); + } + } diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/handler/PaymentStatusCheckerHandlerImpl.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/handler/PaymentStatusCheckerHandlerImpl.java new file mode 100644 index 0000000..e731363 --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/handler/PaymentStatusCheckerHandlerImpl.java @@ -0,0 +1,37 @@ +package com.iprody.adapter.checkstate.handler; + +import com.iprody.adapter.api.XPaymentProviderGateway; +import com.iprody.adapter.dto.CreateChargeResponseDto; +import com.iprody.adapter.mapper.XPaymentMapper; +import com.iprody.api.AsyncSender; +import com.iprody.api.dto.XPaymentAdapterResponseMessage; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +import static com.iprody.api.XPaymentAdapterStatus.CANCELED; +import static com.iprody.api.XPaymentAdapterStatus.SUCCEEDED; + + +@Service +@RequiredArgsConstructor +public class PaymentStatusCheckerHandlerImpl implements PaymentStatusCheckHandler { + + private final AsyncSender asyncSender; + private final XPaymentProviderGateway gateway; + private final XPaymentMapper mapper; + + @Override + public boolean handle(UUID id) { + CreateChargeResponseDto dto = gateway.retrieveCharge(id); + + if (dto != null && (dto.getStatus().equals(SUCCEEDED.name()) || dto.getStatus().equals(CANCELED.name()))) { + asyncSender.send(mapper.responseDtoToKafkaMessage(dto)); + return true; + } else { + return false; + } + } + +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/config/KafkaProperties.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/config/KafkaProperties.java index f2c6aa0..c621bd3 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/config/KafkaProperties.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/config/KafkaProperties.java @@ -2,16 +2,26 @@ import lombok.Data; import lombok.ToString; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; +@Component @Data @ToString -@Component +@PropertySource("classpath:application-kafka.yaml") @ConfigurationProperties(prefix = "app.kafka.topics.xpayment-adapter") public class KafkaProperties { private String requestTopic; private String responseTopic; -} + + public KafkaProperties( + @Value("${request-topic}") String requestTopic, + @Value("${response-topic}") String responseTopic) { + this.requestTopic = requestTopic; + this.responseTopic = responseTopic; + } +} \ No newline at end of file diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/config/XPaymentRestClientConfig.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/config/XPaymentRestClientConfig.java index 53f5734..e424d17 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/config/XPaymentRestClientConfig.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/config/XPaymentRestClientConfig.java @@ -13,9 +13,9 @@ class XPaymentRestClientConfig { @Bean RestTemplate xpaymentRestTemplate( - @Value("${spring.app.xpayment-api.client.username}") String username, - @Value("${spring.app.xpayment-api.client.password}") String password, - @Value("${spring.app.xpayment-api.client.account}") String xPayAccount) { + @Value("${xpayment-api.client.username}") String username, + @Value("${xpayment-api.client.password}") String password, + @Value("${xpayment-api.client.account}") String xPayAccount) { RestTemplate rt = new RestTemplate(); @@ -29,7 +29,7 @@ RestTemplate xpaymentRestTemplate( @Bean ApiClient xpaymentApiClient( - @Value("app.xpayment.client.url") String xPaymentUrl, + @Value("${xpayment-api.client.url}") String xPaymentUrl, RestTemplate xpaymentRestTemplate) { ApiClient apiClient = new ApiClient(xpaymentRestTemplate); diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeRequestDto.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeRequestDto.java index fa1c9d6..59f5b46 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeRequestDto.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeRequestDto.java @@ -1,5 +1,6 @@ package com.iprody.adapter.dto; +import lombok.Getter; import lombok.Setter; import java.math.BigDecimal; @@ -7,6 +8,7 @@ import java.util.Map; import java.util.UUID; +@Getter @Setter public class CreateChargeRequestDto { private BigDecimal amount; diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeResponseDto.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeResponseDto.java index 5d4197e..0c81c3d 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeResponseDto.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/dto/CreateChargeResponseDto.java @@ -1,5 +1,6 @@ package com.iprody.adapter.dto; +import lombok.Builder; import lombok.Getter; import java.math.BigDecimal; @@ -8,6 +9,7 @@ import java.util.UUID; @Getter +@Builder public class CreateChargeResponseDto { private UUID id; private BigDecimal amount; diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentConverter.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentConverter.java new file mode 100644 index 0000000..1b0224a --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentConverter.java @@ -0,0 +1,41 @@ +package com.iprody.adapter.mapper; + +import com.iprody.adapter.dto.CreateChargeRequestDto; +import com.iprody.adapter.dto.CreateChargeResponseDto; +import com.iprody.xpayment.app.api.model.ChargeResponse; +import com.iprody.xpayment.app.api.model.CreateChargeRequest; +import org.springframework.stereotype.Component; + + +@Component +public class XPaymentConverter { + + public CreateChargeResponseDto toCreateChargeResponseDto(ChargeResponse chargeResponse) { + return CreateChargeResponseDto.builder() + .id(chargeResponse.getId()) + .amount(chargeResponse.getAmount()) + .currency(chargeResponse.getCurrency()) + .amountReceived(chargeResponse.getAmountReceived()) + .createdAt(chargeResponse.getCreatedAt()) + .chargedAt(chargeResponse.getChargedAt()) + .customer("test customer") + .order(chargeResponse.getOrder()) + .receiptEmail("abc@test.com") + .status(chargeResponse.getStatus()) + .metadata(chargeResponse.getMetadata()) + .build(); + } + + public CreateChargeRequest toCreateChargeRequest(CreateChargeRequestDto dto) { + CreateChargeRequest result = new CreateChargeRequest(); + result.setOrder(dto.getOrder()); + result.setAmount(dto.getAmount()); + result.setCurrency(dto.getCurrency()); + result.setCustomer("test customer"); + result.setReceiptEmail("abc@test.com"); + result.setMetadata(dto.getMetadata()); + + return result; + } + +} diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentMapper.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentMapper.java index 5b5226a..4fe5d06 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentMapper.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentMapper.java @@ -2,18 +2,34 @@ import com.iprody.adapter.dto.CreateChargeRequestDto; import com.iprody.adapter.dto.CreateChargeResponseDto; +import com.iprody.api.dto.XPaymentAdapterRequestMessage; +import com.iprody.api.dto.XPaymentAdapterResponseMessage; import com.iprody.xpayment.app.api.model.ChargeResponse; import com.iprody.xpayment.app.api.model.CreateChargeRequest; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + @Mapper(componentModel = "spring") public interface XPaymentMapper { - CreateChargeRequestDto toChargeRequestDto(CreateChargeRequest chargeRequest); - +// CreateChargeRequestDto toChargeRequestDto(CreateChargeRequest chargeRequest); +// CreateChargeResponseDto toCreateChargeResponseDto(ChargeResponse chargeResponse); - +// CreateChargeRequest toCreateChargeRequest(CreateChargeRequestDto dto); +// +// ChargeResponse toChargeResponse(CreateChargeResponseDto dto); + +// CreateChargeRequest requestDtoToRequest(CreateChargeRequestDto request); + +// CreateChargeResponseDto responseToResponseDto(ChargeResponse response); + + @Mapping(target = "paymentGuid", source = "order") + @Mapping(target = "transactionRefId", source = "id") + @Mapping(target = "occurredAt", expression = "java(java.time.OffsetDateTime.now())") + XPaymentAdapterResponseMessage responseDtoToKafkaMessage(CreateChargeResponseDto response); - ChargeResponse toChargeResponse(CreateChargeResponseDto dto); + @Mapping(target = "order", source = "paymentGuid") + CreateChargeRequestDto kafkaMessageToRequestDto(XPaymentAdapterRequestMessage request); } diff --git a/xpayment-adapter-app/src/main/resources/application-kafka.yaml b/xpayment-adapter-app/src/main/resources/application-kafka.yaml index 14a809b..afc704a 100644 --- a/xpayment-adapter-app/src/main/resources/application-kafka.yaml +++ b/xpayment-adapter-app/src/main/resources/application-kafka.yaml @@ -33,9 +33,3 @@ spring: request-topic: xpayment-adapter.requests response-topic: xpayment-adapter.responses - xpayment-api: - client: - url: http://localhost:9999 - username: paymentAgentIprody - password: iprodyTestPassword0123 - account: paymentAgentIprodyApiToken \ No newline at end of file diff --git a/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml index f976bf7..eaafda7 100644 --- a/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml +++ b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml @@ -9,6 +9,9 @@ spring: app: rabbitmq: exchange-name: payment-state-check-exchange + delayed-exchange-name: payment-state-check-exchange + dlx-exchange-name: payment.dlx + dlx-routing-key: payment.dead queue-name: payment-state-check-queue max-retries: 60 interval-ms: 60000 \ No newline at end of file diff --git a/xpayment-adapter-app/src/main/resources/application.yaml b/xpayment-adapter-app/src/main/resources/application.yaml index 9cc777e..7b9e369 100644 --- a/xpayment-adapter-app/src/main/resources/application.yaml +++ b/xpayment-adapter-app/src/main/resources/application.yaml @@ -40,4 +40,11 @@ spring: include: health,info,liquibase,beans,metrics,env,loggers endpoint: health: - show-details: always \ No newline at end of file + show-details: always + +xpayment-api: + client: + url: http://localhost:9999 + username: paymentAgentIprody + password: iprodyTestPassword0123 + account: paymentAgentIprodyApiToken From da47b0eab555df1132cc54e0e914ccde6ccac507 Mon Sep 17 00:00:00 2001 From: alexk11 Date: Mon, 2 Feb 2026 23:19:06 +0300 Subject: [PATCH 04/12] added rabbitmq --- .../adapter/api/XPaymentProviderGatewayImpl.java | 6 ------ .../java/com/iprody/adapter/mapper/XPaymentMapper.java | 10 +--------- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/api/XPaymentProviderGatewayImpl.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/api/XPaymentProviderGatewayImpl.java index 700da39..c51c506 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/api/XPaymentProviderGatewayImpl.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/api/XPaymentProviderGatewayImpl.java @@ -18,14 +18,11 @@ class XPaymentProviderGatewayImpl implements XPaymentProviderGateway { private final DefaultApi defaultApi; - private final XPaymentMapper mapper; private final XPaymentConverter converter; public XPaymentProviderGatewayImpl(DefaultApi defaultApi, - XPaymentMapper mapper, XPaymentConverter converter) { this.defaultApi = defaultApi; - this.mapper = mapper; this.converter = converter; } @@ -33,10 +30,8 @@ public XPaymentProviderGatewayImpl(DefaultApi defaultApi, public CreateChargeResponseDto createCharge(CreateChargeRequestDto dto) throws RestClientException { try { - //CreateChargeRequest chargeRequest = mapper.toCreateChargeRequest(dto); CreateChargeRequest chargeRequest = converter.toCreateChargeRequest(dto); ChargeResponse response = defaultApi.createCharge(chargeRequest); - //return mapper.toCreateChargeResponseDto(response); return converter.toCreateChargeResponseDto(response); } catch (Exception e) { throw toRestClientException("POST /charges failed", e); @@ -46,7 +41,6 @@ public CreateChargeResponseDto createCharge(CreateChargeRequestDto dto) @Override public CreateChargeResponseDto retrieveCharge(UUID id) throws RestClientException { try { - //return mapper.toCreateChargeResponseDto(defaultApi.retrieveCharge(id)); return converter.toCreateChargeResponseDto(defaultApi.retrieveCharge(id)); } catch (Exception e) { throw toRestClientException("GET /charges/{id} failed (id=" + id + ")", e); diff --git a/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentMapper.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentMapper.java index 4fe5d06..9530282 100644 --- a/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentMapper.java +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/mapper/XPaymentMapper.java @@ -13,17 +13,9 @@ @Mapper(componentModel = "spring") public interface XPaymentMapper { -// CreateChargeRequestDto toChargeRequestDto(CreateChargeRequest chargeRequest); -// CreateChargeResponseDto toCreateChargeResponseDto(ChargeResponse chargeResponse); -// - CreateChargeRequest toCreateChargeRequest(CreateChargeRequestDto dto); -// -// ChargeResponse toChargeResponse(CreateChargeResponseDto dto); - -// CreateChargeRequest requestDtoToRequest(CreateChargeRequestDto request); -// CreateChargeResponseDto responseToResponseDto(ChargeResponse response); + CreateChargeRequest toCreateChargeRequest(CreateChargeRequestDto dto); @Mapping(target = "paymentGuid", source = "order") @Mapping(target = "transactionRefId", source = "id") From 2cbe18e578735e5477608af901d5cb8eb2eb7f0c Mon Sep 17 00:00:00 2001 From: alexk11 Date: Tue, 3 Feb 2026 13:04:55 +0300 Subject: [PATCH 05/12] refactoring --- .../iprody/controller/PaymentController.java | 10 +- .../src/main/resources/application.yaml | 2 + .../src/main/resources/application-kafka.yaml | 1 - .../main/resources/application-rabbitmq.yaml | 2 +- .../src/main/resources/application.yaml | 4 +- xpayment-api.openapi.yml | 374 +++++++++--------- 6 files changed, 195 insertions(+), 198 deletions(-) diff --git a/payment-service-app/src/main/java/com/iprody/controller/PaymentController.java b/payment-service-app/src/main/java/com/iprody/controller/PaymentController.java index e12fed7..bdf3168 100644 --- a/payment-service-app/src/main/java/com/iprody/controller/PaymentController.java +++ b/payment-service-app/src/main/java/com/iprody/controller/PaymentController.java @@ -13,9 +13,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import java.net.URI; import java.util.List; import java.util.UUID; @@ -58,12 +56,8 @@ public ResponseEntity> fetchAll() { public ResponseEntity addPayment(@RequestBody PaymentDto paymentDto) { log.info("POST: save one payment: \n\n {} \n", paymentDto.toString()); final PaymentDto savedPayment = this.paymentService.createAsync(paymentDto); - final URI location = ServletUriComponentsBuilder.fromCurrentRequest() - .path("/{id}") - .buildAndExpand(savedPayment.getGuid()) - .toUri(); - log.debug("Payment saved: {}", savedPayment); - return ResponseEntity.created(location).body(savedPayment); + log.debug("Payment saved in status: {}", savedPayment.getStatus()); + return ResponseEntity.status(HttpStatus.CREATED).body(savedPayment); } @GetMapping(path = "/{id}") diff --git a/payment-service-app/src/main/resources/application.yaml b/payment-service-app/src/main/resources/application.yaml index 88a0448..9f987ce 100644 --- a/payment-service-app/src/main/resources/application.yaml +++ b/payment-service-app/src/main/resources/application.yaml @@ -1,5 +1,7 @@ server: port: 8082 + error: + include-message: always spring: diff --git a/xpayment-adapter-app/src/main/resources/application-kafka.yaml b/xpayment-adapter-app/src/main/resources/application-kafka.yaml index afc704a..f85dcf6 100644 --- a/xpayment-adapter-app/src/main/resources/application-kafka.yaml +++ b/xpayment-adapter-app/src/main/resources/application-kafka.yaml @@ -32,4 +32,3 @@ spring: xpayment-adapter: request-topic: xpayment-adapter.requests response-topic: xpayment-adapter.responses - diff --git a/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml index eaafda7..4f61d7d 100644 --- a/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml +++ b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml @@ -14,4 +14,4 @@ spring: dlx-routing-key: payment.dead queue-name: payment-state-check-queue max-retries: 60 - interval-ms: 60000 \ No newline at end of file + interval-ms: 60000 diff --git a/xpayment-adapter-app/src/main/resources/application.yaml b/xpayment-adapter-app/src/main/resources/application.yaml index 7b9e369..4b0f108 100644 --- a/xpayment-adapter-app/src/main/resources/application.yaml +++ b/xpayment-adapter-app/src/main/resources/application.yaml @@ -1,12 +1,14 @@ server: port: 8088 + error: + include-message: always spring: application: name: xpayment-adapter-app profiles: - active: kafka, rabbitmq + active: kafka,rabbitmq security: oauth2: resourceserver: diff --git a/xpayment-api.openapi.yml b/xpayment-api.openapi.yml index b327aad..3fccdcf 100644 --- a/xpayment-api.openapi.yml +++ b/xpayment-api.openapi.yml @@ -1,187 +1,187 @@ -openapi: 3.0.3 -info: - title: X Payment API - description: API for creating and retrieving charges - version: 1.0.0 -servers: - - url: https://api.xpayment.com/v1 - description: X Payment API server (Production) - - url: http://localhost:8080 - description: X Payment API server (Development) -paths: - /charges: - post: - summary: Create a charge - description: Create a new charge - operationId: createCharge - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/CreateChargeRequest' - responses: - '201': - description: Charge created successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ChargeResponse' - '401': - description: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '402': - description: Request Failed. The parameters were valid but the request failed. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - /charges/{id}: - get: - summary: Retrieve a charge - description: Retrieve a charge by its ID - operationId: retrieveCharge - parameters: - - name: id - in: path - required: true - schema: - type: string - description: Unique identifier for the object. Must conform to UUID format. - format: uuid - responses: - '200': - description: Charge retrieved successfully - content: - application/json: - schema: - $ref: '#/components/schemas/ChargeResponse' - '401': - description: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' - '404': - description: Not Found. The requested resource doesn’t exist. - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponse' -components: - schemas: - CreateChargeRequest: - type: object - properties: - amount: - type: string - format: decimal - description: The amount to be charged by this payment, in decimal format (e.g., 100.50 for $100.50). - example: "1090.50" - currency: - type: string - description: Three-letter ISO currency code, in uppercase. Must conform with ISO 4217. - example: USD - customer: - type: string - description: Full name of the customer this payment belongs to. - example: Henry Ford - order: - type: string - description: Unique identifier of the order this payment belongs to. Also it will be used as idempotency-key. - format: uuid - receiptEmail: - type: string - description: Email address to send the receipt to. - example: test@email.com - metadata: - type: object - description: Set of key-value pairs that you can attach to an object. - additionalProperties: true - required: - - id - - amount - - currency - - customer - - order - - receiptEmail - ChargeResponse: - type: object - properties: - id: - type: string - description: Unique identifier for the charge a the inquired payment belongs to. Must conform to UUID format. - format: uuid - amount: - type: string - format: decimal - description: The amount to be charged by this payment, in decimal format (e.g., 100.50 for $100.50). - example: "2000.00" - currency: - type: string - description: Three-letter ISO currency code, in uppercase. Must conform with ISO 4217. - example: USD - amountReceived: - type: string - format: decimal - description: Amount eventually charged by this payment. - example: "0.00" - createdAt: - type: string - description: Time at which the payment was created but not yet charged. Must conform ISO 8601. - example: 2011-12-03T10:15:30Z - chargedAt: - type: string - description: Time at which the payment was created. Must conform ISO 8601. - example: 2011-12-03T10:15:30Z - customer: - type: string - description: Full name of the customer this payment belongs to. - example: Henry Ford - order: - type: string - description: Unique identifier of the order this payment belongs to. - format: uuid - receiptEmail: - type: string - description: Email address to send the receipt to. - example: henry.ford@email.com - status: - type: string - description: Status of this payment, one of processing, canceled, or succeeded. - example: processing - metadata: - type: object - description: Set of key-value pairs that you can attach to a payment. - additionalProperties: true - ErrorResponse: - type: object - properties: - statusCode: - type: integer - description: HTTP status code of the error. - example: 401 - message: - type: string - description: Error message describing the error. - example: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. - chargeId: - type: string - description: Unique identifier of the order related to the error. - format: uuid - securitySchemes: - X-Pay-Account: - type: apiKey - in: header - name: X-Pay-Account - description: To act as connected accounts, clients can issue requests using the X-Pay-Account special header. Make sure that this header contains a pre-generated XPAY Account ID. - BasicAuth: - type: http - scheme: basic - description: Basic authentication using username and password. -security: - - X-Pay-Account: [] - - BasicAuth: [] \ No newline at end of file +#openapi: 3.0.3 +#info: +# title: X Payment API +# description: API for creating and retrieving charges +# version: 1.0.0 +#servers: +# - url: https://api.xpayment.com/v1 +# description: X Payment API server (Production) +# - url: http://localhost:8080 +# description: X Payment API server (Development) +#paths: +# /charges: +# post: +# summary: Create a charge +# description: Create a new charge +# operationId: createCharge +# requestBody: +# required: true +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/CreateChargeRequest' +# responses: +# '201': +# description: Charge created successfully +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ChargeResponse' +# '401': +# description: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' +# '402': +# description: Request Failed. The parameters were valid but the request failed. +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' +# /charges/{id}: +# get: +# summary: Retrieve a charge +# description: Retrieve a charge by its ID +# operationId: retrieveCharge +# parameters: +# - name: id +# in: path +# required: true +# schema: +# type: string +# description: Unique identifier for the object. Must conform to UUID format. +# format: uuid +# responses: +# '200': +# description: Charge retrieved successfully +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ChargeResponse' +# '401': +# description: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' +# '404': +# description: Not Found. The requested resource doesn’t exist. +# content: +# application/json: +# schema: +# $ref: '#/components/schemas/ErrorResponse' +#components: +# schemas: +# CreateChargeRequest: +# type: object +# properties: +# amount: +# type: string +# format: decimal +# description: The amount to be charged by this payment, in decimal format (e.g., 100.50 for $100.50). +# example: "1090.50" +# currency: +# type: string +# description: Three-letter ISO currency code, in uppercase. Must conform with ISO 4217. +# example: USD +# customer: +# type: string +# description: Full name of the customer this payment belongs to. +# example: Henry Ford +# order: +# type: string +# description: Unique identifier of the order this payment belongs to. Also it will be used as idempotency-key. +# format: uuid +# receiptEmail: +# type: string +# description: Email address to send the receipt to. +# example: test@email.com +# metadata: +# type: object +# description: Set of key-value pairs that you can attach to an object. +# additionalProperties: true +# required: +# - id +# - amount +# - currency +# - customer +# - order +# - receiptEmail +# ChargeResponse: +# type: object +# properties: +# id: +# type: string +# description: Unique identifier for the charge a the inquired payment belongs to. Must conform to UUID format. +# format: uuid +# amount: +# type: string +# format: decimal +# description: The amount to be charged by this payment, in decimal format (e.g., 100.50 for $100.50). +# example: "2000.00" +# currency: +# type: string +# description: Three-letter ISO currency code, in uppercase. Must conform with ISO 4217. +# example: USD +# amountReceived: +# type: string +# format: decimal +# description: Amount eventually charged by this payment. +# example: "0.00" +# createdAt: +# type: string +# description: Time at which the payment was created but not yet charged. Must conform ISO 8601. +# example: 2011-12-03T10:15:30Z +# chargedAt: +# type: string +# description: Time at which the payment was created. Must conform ISO 8601. +# example: 2011-12-03T10:15:30Z +# customer: +# type: string +# description: Full name of the customer this payment belongs to. +# example: Henry Ford +# order: +# type: string +# description: Unique identifier of the order this payment belongs to. +# format: uuid +# receiptEmail: +# type: string +# description: Email address to send the receipt to. +# example: henry.ford@email.com +# status: +# type: string +# description: Status of this payment, one of processing, canceled, or succeeded. +# example: processing +# metadata: +# type: object +# description: Set of key-value pairs that you can attach to a payment. +# additionalProperties: true +# ErrorResponse: +# type: object +# properties: +# statusCode: +# type: integer +# description: HTTP status code of the error. +# example: 401 +# message: +# type: string +# description: Error message describing the error. +# example: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. +# chargeId: +# type: string +# description: Unique identifier of the order related to the error. +# format: uuid +# securitySchemes: +# X-Pay-Account: +# type: apiKey +# in: header +# name: X-Pay-Account +# description: To act as connected accounts, clients can issue requests using the X-Pay-Account special header. Make sure that this header contains a pre-generated XPAY Account ID. +# BasicAuth: +# type: http +# scheme: basic +# description: Basic authentication using username and password. +#security: +# - X-Pay-Account: [] +# - BasicAuth: [] \ No newline at end of file From 6e225c3b3c293921ea7502b099c3be00609c4423 Mon Sep 17 00:00:00 2001 From: alexk11 Date: Tue, 3 Feb 2026 13:06:01 +0300 Subject: [PATCH 06/12] refactoring --- xpayment-api.openapi.yml | 187 --------------------------------------- 1 file changed, 187 deletions(-) delete mode 100644 xpayment-api.openapi.yml diff --git a/xpayment-api.openapi.yml b/xpayment-api.openapi.yml deleted file mode 100644 index 3fccdcf..0000000 --- a/xpayment-api.openapi.yml +++ /dev/null @@ -1,187 +0,0 @@ -#openapi: 3.0.3 -#info: -# title: X Payment API -# description: API for creating and retrieving charges -# version: 1.0.0 -#servers: -# - url: https://api.xpayment.com/v1 -# description: X Payment API server (Production) -# - url: http://localhost:8080 -# description: X Payment API server (Development) -#paths: -# /charges: -# post: -# summary: Create a charge -# description: Create a new charge -# operationId: createCharge -# requestBody: -# required: true -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/CreateChargeRequest' -# responses: -# '201': -# description: Charge created successfully -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/ChargeResponse' -# '401': -# description: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/ErrorResponse' -# '402': -# description: Request Failed. The parameters were valid but the request failed. -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/ErrorResponse' -# /charges/{id}: -# get: -# summary: Retrieve a charge -# description: Retrieve a charge by its ID -# operationId: retrieveCharge -# parameters: -# - name: id -# in: path -# required: true -# schema: -# type: string -# description: Unique identifier for the object. Must conform to UUID format. -# format: uuid -# responses: -# '200': -# description: Charge retrieved successfully -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/ChargeResponse' -# '401': -# description: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/ErrorResponse' -# '404': -# description: Not Found. The requested resource doesn’t exist. -# content: -# application/json: -# schema: -# $ref: '#/components/schemas/ErrorResponse' -#components: -# schemas: -# CreateChargeRequest: -# type: object -# properties: -# amount: -# type: string -# format: decimal -# description: The amount to be charged by this payment, in decimal format (e.g., 100.50 for $100.50). -# example: "1090.50" -# currency: -# type: string -# description: Three-letter ISO currency code, in uppercase. Must conform with ISO 4217. -# example: USD -# customer: -# type: string -# description: Full name of the customer this payment belongs to. -# example: Henry Ford -# order: -# type: string -# description: Unique identifier of the order this payment belongs to. Also it will be used as idempotency-key. -# format: uuid -# receiptEmail: -# type: string -# description: Email address to send the receipt to. -# example: test@email.com -# metadata: -# type: object -# description: Set of key-value pairs that you can attach to an object. -# additionalProperties: true -# required: -# - id -# - amount -# - currency -# - customer -# - order -# - receiptEmail -# ChargeResponse: -# type: object -# properties: -# id: -# type: string -# description: Unique identifier for the charge a the inquired payment belongs to. Must conform to UUID format. -# format: uuid -# amount: -# type: string -# format: decimal -# description: The amount to be charged by this payment, in decimal format (e.g., 100.50 for $100.50). -# example: "2000.00" -# currency: -# type: string -# description: Three-letter ISO currency code, in uppercase. Must conform with ISO 4217. -# example: USD -# amountReceived: -# type: string -# format: decimal -# description: Amount eventually charged by this payment. -# example: "0.00" -# createdAt: -# type: string -# description: Time at which the payment was created but not yet charged. Must conform ISO 8601. -# example: 2011-12-03T10:15:30Z -# chargedAt: -# type: string -# description: Time at which the payment was created. Must conform ISO 8601. -# example: 2011-12-03T10:15:30Z -# customer: -# type: string -# description: Full name of the customer this payment belongs to. -# example: Henry Ford -# order: -# type: string -# description: Unique identifier of the order this payment belongs to. -# format: uuid -# receiptEmail: -# type: string -# description: Email address to send the receipt to. -# example: henry.ford@email.com -# status: -# type: string -# description: Status of this payment, one of processing, canceled, or succeeded. -# example: processing -# metadata: -# type: object -# description: Set of key-value pairs that you can attach to a payment. -# additionalProperties: true -# ErrorResponse: -# type: object -# properties: -# statusCode: -# type: integer -# description: HTTP status code of the error. -# example: 401 -# message: -# type: string -# description: Error message describing the error. -# example: Unauthorized. Either X-Pay-Account or Authorization or both headers were not provided or invalid. -# chargeId: -# type: string -# description: Unique identifier of the order related to the error. -# format: uuid -# securitySchemes: -# X-Pay-Account: -# type: apiKey -# in: header -# name: X-Pay-Account -# description: To act as connected accounts, clients can issue requests using the X-Pay-Account special header. Make sure that this header contains a pre-generated XPAY Account ID. -# BasicAuth: -# type: http -# scheme: basic -# description: Basic authentication using username and password. -#security: -# - X-Pay-Account: [] -# - BasicAuth: [] \ No newline at end of file From 6c191ca31326203befca77f631889cea14755227 Mon Sep 17 00:00:00 2001 From: alexk11 Date: Tue, 3 Feb 2026 15:23:39 +0300 Subject: [PATCH 07/12] pack adapter app into docker image --- docker-compose.yml | 12 ++++++++ xpayment-adapter-app/pom.xml | 29 +++++++++++++++++-- .../src/main/resources/README.txt | 5 ++++ .../src/main/resources/application-kafka.yaml | 2 +- .../main/resources/application-rabbitmq.yaml | 2 +- .../src/main/resources/application.yaml | 2 +- 6 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 xpayment-adapter-app/src/main/resources/README.txt diff --git a/docker-compose.yml b/docker-compose.yml index ad4f13c..7a03f1a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,18 @@ services: KEYCLOAK_HOST: keycloak:8080 restart: unless-stopped + xpayment-adapter-app: + image: xpayment-adapter-app + build: + context: xpayment-adapter-app + dockerfile: xpayment-adapter-app/Dockerfile + ports: + - "8088:8080" + depends_on: + postgres: + condition: service_healthy + restart: unless-stopped + postgres: image: postgres:16 container_name: postgres-db diff --git a/xpayment-adapter-app/pom.xml b/xpayment-adapter-app/pom.xml index 06fdc77..24d39ed 100644 --- a/xpayment-adapter-app/pom.xml +++ b/xpayment-adapter-app/pom.xml @@ -9,10 +9,12 @@ ../pom.xml - xpayment-service-adapter-app + xpayment-adapter-app 0.0.1-SNAPSHOT - payment-service-adapter-app - xpayment-adapter-app + payment-adapter-app + XPayment Service Adapter App + + jar @@ -125,6 +127,20 @@ + + maven-assembly-plugin + + + + com.iprody.adapter.XPaymentAdapterAppApplication + + + + jar-with-dependencies + + false + + org.springframework.boot spring-boot-maven-plugin @@ -137,6 +153,13 @@ + + + + repackage + + + org.openapitools diff --git a/xpayment-adapter-app/src/main/resources/README.txt b/xpayment-adapter-app/src/main/resources/README.txt new file mode 100644 index 0000000..6544b31 --- /dev/null +++ b/xpayment-adapter-app/src/main/resources/README.txt @@ -0,0 +1,5 @@ +Compile jar file with +"mvn clean compile assembly:single" to avoid the error "no main manifest attribute in app.jar" + +Build docker image: + docker build --build-arg JAR_FILE=.//target//xpayment-adapter-app-0.0.1-SNAPSHOT.jar -t xpayment-adapter-app . diff --git a/xpayment-adapter-app/src/main/resources/application-kafka.yaml b/xpayment-adapter-app/src/main/resources/application-kafka.yaml index f85dcf6..7080727 100644 --- a/xpayment-adapter-app/src/main/resources/application-kafka.yaml +++ b/xpayment-adapter-app/src/main/resources/application-kafka.yaml @@ -1,7 +1,7 @@ spring: kafka: - bootstrap-servers: localhost:9093 + bootstrap-servers: kafka:9092 producer: # Acknowledgment level: # "0": Producer does not wait for any acknowledgment from the broker (highest throughput, lowest durability). diff --git a/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml index 4f61d7d..93dd106 100644 --- a/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml +++ b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml @@ -1,7 +1,7 @@ spring: rabbitmq: - host: localhost + host: rabbitmq port: 5672 username: admin password: admin diff --git a/xpayment-adapter-app/src/main/resources/application.yaml b/xpayment-adapter-app/src/main/resources/application.yaml index 4b0f108..3fea5fd 100644 --- a/xpayment-adapter-app/src/main/resources/application.yaml +++ b/xpayment-adapter-app/src/main/resources/application.yaml @@ -46,7 +46,7 @@ spring: xpayment-api: client: - url: http://localhost:9999 + url: http://xpayment-api.xpayment-api-1:9999 username: paymentAgentIprody password: iprodyTestPassword0123 account: paymentAgentIprodyApiToken From d93044390d6dd78353a2c7d09b382997ef535e45 Mon Sep 17 00:00:00 2001 From: Alexey Kryuchkov Date: Wed, 4 Feb 2026 16:40:12 +0300 Subject: [PATCH 08/12] added k8s --- k8s/k8s-demo.yml | 39 ++++++++++++++++++++++++++ k8s/kafka-values.yml | 12 ++++++++ k8s/keycloak-values.yml | 30 ++++++++++++++++++++ k8s/payment-service.yml | 48 ++++++++++++++++++++++++++++++++ k8s/xpayment-adapter-service.yml | 43 ++++++++++++++++++++++++++++ 5 files changed, 172 insertions(+) create mode 100644 k8s/k8s-demo.yml create mode 100644 k8s/kafka-values.yml create mode 100644 k8s/keycloak-values.yml create mode 100644 k8s/payment-service.yml create mode 100644 k8s/xpayment-adapter-service.yml diff --git a/k8s/k8s-demo.yml b/k8s/k8s-demo.yml new file mode 100644 index 0000000..db4f02f --- /dev/null +++ b/k8s/k8s-demo.yml @@ -0,0 +1,39 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment # имя Deployment +spec: + replicas: 3 # количество реплик (Pod’ов) + selector: # селектор для идентификации Pod’ов + matchLabels: + app: nginx # выбираем Pod’ы с меткой app=nginx + template: # шаблон для создания Pod’ов + metadata: + labels: + app: nginx # метка, чтобы Pod соответствовал селектору + spec: # спецификация Pod’а + containers: # список контейнеров внутри Pod’а + - name: nginx # имя контейнера + image: nginx:1.25 # образ контейнера (nginx версии 1.25) + ports: + - containerPort: 80 # порт, который будет слушать контейнер (внутри Pod) + +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx-service +spec: + selector: + app: nginx # выбираем Pod’ы с меткой app=nginx + ports: # список портов + - protocol: TCP # протокол (TCP по умолчанию) + port: 80 # порт Service + targetPort: 80 # порт внутри Pod’а + type: ClusterIP # тип Service: + +# ClusterIP (по умолчанию) -- доступ только изнутри кластера + +# NodePort -- открывает порт на нодах для внешнего доступа + +# LoadBalancer -- для облачных провайдеров, создаёт балансировщик \ No newline at end of file diff --git a/k8s/kafka-values.yml b/k8s/kafka-values.yml new file mode 100644 index 0000000..63c0c70 --- /dev/null +++ b/k8s/kafka-values.yml @@ -0,0 +1,12 @@ +kraft: + enabled: true # режим KRaft (по умолчанию true в новых релизах) + +controller: + replicaCount: 1 # один контроллер для стенда + +broker: + replicaCount: 1 # один брокер + +listeners: + client: + protocol: PLAINTEXT # для простоты: без TLS/SASL на учебном стенде diff --git a/k8s/keycloak-values.yml b/k8s/keycloak-values.yml new file mode 100644 index 0000000..db30dca --- /dev/null +++ b/k8s/keycloak-values.yml @@ -0,0 +1,30 @@ +image: + registry: quay.io + repository: keycloak/keycloak + tag: 24.0.3 + +extraEnvVars: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin + +command: + - start-dev + - --http-port=8080 + - --import-realm + - --log-level=DEBUG + +service: + ports: + http: 8085 + +extraVolumes: + - name: realm-import + configMap: + name: keycloak-realm + +extraVolumeMounts: + - name: realm-import + mountPath: /opt/keycloak/data/import/realm-export.json + subPath: realm-export.json \ No newline at end of file diff --git a/k8s/payment-service.yml b/k8s/payment-service.yml new file mode 100644 index 0000000..6a84c4d --- /dev/null +++ b/k8s/payment-service.yml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: payment-service-app + labels: + app: payment-service-app +spec: + replicas: 1 # одно приложение (можно увеличить) + selector: + matchLabels: + app: payment-service-app + template: + metadata: + labels: + app: payment-service-app + spec: + containers: + - name: payment-service-app + image: payment-service:latest # докер образ + ports: + - containerPort: 8080 # порт внутри Pod’а + env: # переменные окружения + - name: SPRING_DATASOURCE_URL + value: jdbc:postgresql://my-pg-postgresql.db.svc.cluster.local:5432/payment-db + - name: SPRING_DATASOURCE_USERNAME + value: postgres + - name: SPRING_DATASOURCE_PASSWORD + value: secret + - name: SPRING_KAFKA_BOOTSTRAP_SERVERS + value: my-kafka.kafka.svc.cluster.local:9092 + - name: SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI + value: http://keycloak.keycloak.svc.cluster.local:8080/realms/iprody-lms +--- +apiVersion: v1 +kind: Service +metadata: + name: payment-service-app + labels: + app: payment-service-app +spec: + selector: + app: payment-service-app + ports: + - protocol: TCP + port: 8080 # порт внутри кластера + targetPort: 8080 # порт контейнера + nodePort: 30080 # внешний порт (только если type=NodePort) + type: NodePort # вариант для доступа снаружи (Minikube, DockerDesktop) \ No newline at end of file diff --git a/k8s/xpayment-adapter-service.yml b/k8s/xpayment-adapter-service.yml new file mode 100644 index 0000000..c6ab644 --- /dev/null +++ b/k8s/xpayment-adapter-service.yml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xpayment-adapter-app +spec: + replicas: 1 + selector: + matchLabels: + app: x-payment-adapter-app + template: + metadata: + labels: + app: x-payment-adapter-app + spec: + containers: + - name: x-payment-adapter-app + image: x-payment-adapter-app:latest + ports: + - containerPort: 8080 + env: + - name: SPRING_KAFKA_BOOTSTRAP_SERVERS + value: "my-kafka.kafka.svc.cluster.local:9093" + - name: SPRING_KAFKA_CONSUMER_GROUP_ID + value: "xpayment-adapter-result-consumers" + - name: APP_KAFKA_TOPIC_REQUEST + value: "xpayment-adapter.requests" + - name: APP_KAFKA_TOPIC_RESPONSE + value: "xpayment-adapter.responses" + - name: RABBITMQ_URL + value: "amqp://rabbitmq.rabbitmq.svc.cluster.local:5672" +--- +apiVersion: v1 +kind: Service +metadata: + name: xpayment-adapter-app +spec: + selector: + app: x-payment-adapter-app + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + type: ClusterIP \ No newline at end of file From 65d7dfeb8b6d234c34f81f7e8a32264129126b2a Mon Sep 17 00:00:00 2001 From: alexk11 Date: Wed, 4 Feb 2026 21:47:25 +0300 Subject: [PATCH 09/12] added k8s deployment possibility --- payment-service-app/k8s/payment-service.yml | 48 +++++++++++++++++++ .../src/main/resources/application-db.yaml | 4 +- xpayment-adapter-app/k8s/k8s-nginx-values.yml | 38 +++++++++++++++ xpayment-adapter-app/k8s/kafka-values.yml | 12 +++++ xpayment-adapter-app/k8s/keycloak-values.yml | 30 ++++++++++++ .../k8s/payment-service-values.yml | 48 +++++++++++++++++++ .../k8s/xpayment-adapter-values.yml | 43 +++++++++++++++++ .../src/main/resources/README.txt | 4 ++ 8 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 payment-service-app/k8s/payment-service.yml create mode 100644 xpayment-adapter-app/k8s/k8s-nginx-values.yml create mode 100644 xpayment-adapter-app/k8s/kafka-values.yml create mode 100644 xpayment-adapter-app/k8s/keycloak-values.yml create mode 100644 xpayment-adapter-app/k8s/payment-service-values.yml create mode 100644 xpayment-adapter-app/k8s/xpayment-adapter-values.yml diff --git a/payment-service-app/k8s/payment-service.yml b/payment-service-app/k8s/payment-service.yml new file mode 100644 index 0000000..ef0ee07 --- /dev/null +++ b/payment-service-app/k8s/payment-service.yml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: payment-service-app + labels: + app: payment-service-app +spec: + replicas: 1 # одно приложение (можно увеличить) + selector: + matchLabels: + app: payment-service-app + template: + metadata: + labels: + app: payment-service-app + spec: + containers: + - name: payment-service-app + image: payment-service:latest # докер образ + ports: + - containerPort: 8080 # порт внутри Pod’а + env: + - name: SPRING_DATASOURCE_URL + value: jdbc:postgresql://my-pg-postgresql.db.svc.cluster.local:5432/payment-db + - name: SPRING_DATASOURCE_USERNAME + value: postgres + - name: SPRING_DATASOURCE_PASSWORD + value: secret + - name: SPRING_KAFKA_BOOTSTRAP_SERVERS + value: my-kafka.kafka.svc.cluster.local:9092 + - name: SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI + value: http://keycloak.keycloak.svc.cluster.local:8080/realms/iprody-lms +--- +apiVersion: v1 +kind: Service +metadata: + name: payment-service-app + labels: + app: payment-service-app +spec: + selector: + app: payment-service-app + ports: + - protocol: TCP + port: 8080 # порт внутри кластера + targetPort: 8080 # порт контейнера + nodePort: 30080 # внешний порт (только если type=NodePort) + type: NodePort # вариант для доступа снаружи (Minikube, DockerDesktop) diff --git a/payment-service-app/src/main/resources/application-db.yaml b/payment-service-app/src/main/resources/application-db.yaml index 32db3a1..e916081 100644 --- a/payment-service-app/src/main/resources/application-db.yaml +++ b/payment-service-app/src/main/resources/application-db.yaml @@ -1,8 +1,8 @@ spring: datasource: - url: jdbc:postgresql://localhost:5432/payment-db # URL подключения к БД - #url: jdbc:postgresql://postgres-db:5432/payment-db + #url: jdbc:postgresql://localhost:5432/payment-db # URL подключения к БД + url: jdbc:postgresql://postgres-db:5432/payment-db username: user # Имя пользователя password: secret # Пароль driver-class-name: org.postgresql.Driver # класс jdbc драйвера diff --git a/xpayment-adapter-app/k8s/k8s-nginx-values.yml b/xpayment-adapter-app/k8s/k8s-nginx-values.yml new file mode 100644 index 0000000..cfb085d --- /dev/null +++ b/xpayment-adapter-app/k8s/k8s-nginx-values.yml @@ -0,0 +1,38 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment # имя Deployment +spec: + replicas: 3 # количество реплик (Pod’ов) + selector: # селектор для идентификации Pod’ов + matchLabels: + app: nginx # выбираем Pod’ы с меткой app=nginx + template: # шаблон для создания Pod’ов + metadata: + labels: + app: nginx # метка, чтобы Pod соответствовал селектору + spec: # спецификация Pod’а + containers: # список контейнеров внутри Pod’а + - name: nginx # имя контейнера + image: nginx:1.25 # образ контейнера (nginx версии 1.25) + ports: + - containerPort: 80 # порт, который будет слушать контейнер (внутри Pod) +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx-service +spec: + selector: + app: nginx # выбираем Pod’ы с меткой app=nginx + ports: # список портов + - protocol: TCP # протокол (TCP по умолчанию) + port: 80 # порт Service + targetPort: 80 # порт внутри Pod’а + type: ClusterIP # тип Service: + + # ClusterIP (по умолчанию) -- доступ только изнутри кластера + + # NodePort -- открывает порт на нодах для внешнего доступа + + # LoadBalancer -- для облачных провайдеров, создаёт балансировщик \ No newline at end of file diff --git a/xpayment-adapter-app/k8s/kafka-values.yml b/xpayment-adapter-app/k8s/kafka-values.yml new file mode 100644 index 0000000..77ec8e3 --- /dev/null +++ b/xpayment-adapter-app/k8s/kafka-values.yml @@ -0,0 +1,12 @@ +kraft: + enabled: true # режим KRaft (по умолчанию true в новых релизах) + +controller: + replicaCount: 1 # один контроллер для стенда + +broker: + replicaCount: 1 # один брокер + +listeners: + client: + protocol: PLAINTEXT # для простоты: без TLS/SASL на учебном стенде \ No newline at end of file diff --git a/xpayment-adapter-app/k8s/keycloak-values.yml b/xpayment-adapter-app/k8s/keycloak-values.yml new file mode 100644 index 0000000..db30dca --- /dev/null +++ b/xpayment-adapter-app/k8s/keycloak-values.yml @@ -0,0 +1,30 @@ +image: + registry: quay.io + repository: keycloak/keycloak + tag: 24.0.3 + +extraEnvVars: + - name: KEYCLOAK_ADMIN + value: admin + - name: KEYCLOAK_ADMIN_PASSWORD + value: admin + +command: + - start-dev + - --http-port=8080 + - --import-realm + - --log-level=DEBUG + +service: + ports: + http: 8085 + +extraVolumes: + - name: realm-import + configMap: + name: keycloak-realm + +extraVolumeMounts: + - name: realm-import + mountPath: /opt/keycloak/data/import/realm-export.json + subPath: realm-export.json \ No newline at end of file diff --git a/xpayment-adapter-app/k8s/payment-service-values.yml b/xpayment-adapter-app/k8s/payment-service-values.yml new file mode 100644 index 0000000..df8710e --- /dev/null +++ b/xpayment-adapter-app/k8s/payment-service-values.yml @@ -0,0 +1,48 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: payment-service-app + labels: + app: payment-service-app +spec: + replicas: 1 # одно приложение (можно увеличить) + selector: + matchLabels: + app: payment-service-app + template: + metadata: + labels: + app: payment-service-app + spec: + containers: + - name: payment-service-app + image: payment-service:latest # докер образ + ports: + - containerPort: 8080 # порт внутри Pod’а + env: + - name: SPRING_DATASOURCE_URL + value: jdbc:postgresql://my-pg-postgresql.db.svc.cluster.local:5432/payment-db + - name: SPRING_DATASOURCE_USERNAME + value: postgres + - name: SPRING_DATASOURCE_PASSWORD + value: secret + - name: SPRING_KAFKA_BOOTSTRAP_SERVERS + value: my-kafka.kafka.svc.cluster.local:9092 + - name: SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI + value: http://keycloak.keycloak.svc.cluster.local:8080/realms/iprody-lms +--- +apiVersion: v1 +kind: Service +metadata: + name: payment-service-app + labels: + app: payment-service-app +spec: + selector: + app: payment-service-app + ports: + - protocol: TCP + port: 8080 # порт внутри кластера + targetPort: 8080 # порт контейнера + nodePort: 30080 # внешний порт (только если type=NodePort) + type: NodePort # вариант для доступа снаружи (Minikube, DockerDesktop) diff --git a/xpayment-adapter-app/k8s/xpayment-adapter-values.yml b/xpayment-adapter-app/k8s/xpayment-adapter-values.yml new file mode 100644 index 0000000..9b27872 --- /dev/null +++ b/xpayment-adapter-app/k8s/xpayment-adapter-values.yml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: xpayment-adapter-app +spec: + replicas: 1 + selector: + matchLabels: + app: xpayment-adapter-app + template: + metadata: + labels: + app: xpayment-adapter-app + spec: + containers: + - name: xpayment-adapter-app + image: xpayment-adapter-app:latest + ports: + - containerPort: 8080 + env: + - name: SPRING_KAFKA_BOOTSTRAP_SERVERS + value: "my-kafka.kafka.svc.cluster.local:9093" + - name: SPRING_KAFKA_CONSUMER_GROUP_ID + value: "xpayment-adapter-result-consumers" + - name: APP_KAFKA_TOPIC_REQUEST + value: "xpayment-adapter.requests" + - name: APP_KAFKA_TOPIC_RESPONSE + value: "xpayment-adapter.responses" + - name: RABBITMQ_URL + value: "amqp://rabbitmq.rabbitmq.svc.cluster.local:5672" +--- +apiVersion: v1 +kind: Service +metadata: + name: xpayment-adapter-app +spec: + selector: + app: xpayment-adapter-app + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + type: ClusterIP \ No newline at end of file diff --git a/xpayment-adapter-app/src/main/resources/README.txt b/xpayment-adapter-app/src/main/resources/README.txt index 6544b31..4b1d118 100644 --- a/xpayment-adapter-app/src/main/resources/README.txt +++ b/xpayment-adapter-app/src/main/resources/README.txt @@ -3,3 +3,7 @@ Compile jar file with Build docker image: docker build --build-arg JAR_FILE=.//target//xpayment-adapter-app-0.0.1-SNAPSHOT.jar -t xpayment-adapter-app . + +K8S: + + helm install keycloak bitnami/keycloak -f keycloak-values.yml --set global.security.allowInsecureImages=true \ No newline at end of file From 3c1d2802363b5e991a40237218a7278a98cda43d Mon Sep 17 00:00:00 2001 From: alexk11 Date: Wed, 4 Feb 2026 21:49:08 +0300 Subject: [PATCH 10/12] added k8s deployment possibility --- .../k8s => k8s}/k8s-nginx-values.yml | 0 k8s/kafka-values.yml | 2 +- .../k8s => k8s}/payment-service-values.yml | 0 .../k8s => k8s}/xpayment-adapter-values.yml | 0 {k8s => k8s_xx}/k8s-demo.yml | 0 .../k8s => k8s_xx}/kafka-values.yml | 2 +- .../k8s => k8s_xx}/keycloak-values.yml | 0 {k8s => k8s_xx}/payment-service.yml | 0 {k8s => k8s_xx}/xpayment-adapter-service.yml | 0 payment-service-app/k8s/payment-service.yml | 48 ------------------- 10 files changed, 2 insertions(+), 50 deletions(-) rename {xpayment-adapter-app/k8s => k8s}/k8s-nginx-values.yml (100%) rename {xpayment-adapter-app/k8s => k8s}/payment-service-values.yml (100%) rename {xpayment-adapter-app/k8s => k8s}/xpayment-adapter-values.yml (100%) rename {k8s => k8s_xx}/k8s-demo.yml (100%) rename {xpayment-adapter-app/k8s => k8s_xx}/kafka-values.yml (89%) rename {xpayment-adapter-app/k8s => k8s_xx}/keycloak-values.yml (100%) rename {k8s => k8s_xx}/payment-service.yml (100%) rename {k8s => k8s_xx}/xpayment-adapter-service.yml (100%) delete mode 100644 payment-service-app/k8s/payment-service.yml diff --git a/xpayment-adapter-app/k8s/k8s-nginx-values.yml b/k8s/k8s-nginx-values.yml similarity index 100% rename from xpayment-adapter-app/k8s/k8s-nginx-values.yml rename to k8s/k8s-nginx-values.yml diff --git a/k8s/kafka-values.yml b/k8s/kafka-values.yml index 63c0c70..77ec8e3 100644 --- a/k8s/kafka-values.yml +++ b/k8s/kafka-values.yml @@ -9,4 +9,4 @@ broker: listeners: client: - protocol: PLAINTEXT # для простоты: без TLS/SASL на учебном стенде + protocol: PLAINTEXT # для простоты: без TLS/SASL на учебном стенде \ No newline at end of file diff --git a/xpayment-adapter-app/k8s/payment-service-values.yml b/k8s/payment-service-values.yml similarity index 100% rename from xpayment-adapter-app/k8s/payment-service-values.yml rename to k8s/payment-service-values.yml diff --git a/xpayment-adapter-app/k8s/xpayment-adapter-values.yml b/k8s/xpayment-adapter-values.yml similarity index 100% rename from xpayment-adapter-app/k8s/xpayment-adapter-values.yml rename to k8s/xpayment-adapter-values.yml diff --git a/k8s/k8s-demo.yml b/k8s_xx/k8s-demo.yml similarity index 100% rename from k8s/k8s-demo.yml rename to k8s_xx/k8s-demo.yml diff --git a/xpayment-adapter-app/k8s/kafka-values.yml b/k8s_xx/kafka-values.yml similarity index 89% rename from xpayment-adapter-app/k8s/kafka-values.yml rename to k8s_xx/kafka-values.yml index 77ec8e3..63c0c70 100644 --- a/xpayment-adapter-app/k8s/kafka-values.yml +++ b/k8s_xx/kafka-values.yml @@ -9,4 +9,4 @@ broker: listeners: client: - protocol: PLAINTEXT # для простоты: без TLS/SASL на учебном стенде \ No newline at end of file + protocol: PLAINTEXT # для простоты: без TLS/SASL на учебном стенде diff --git a/xpayment-adapter-app/k8s/keycloak-values.yml b/k8s_xx/keycloak-values.yml similarity index 100% rename from xpayment-adapter-app/k8s/keycloak-values.yml rename to k8s_xx/keycloak-values.yml diff --git a/k8s/payment-service.yml b/k8s_xx/payment-service.yml similarity index 100% rename from k8s/payment-service.yml rename to k8s_xx/payment-service.yml diff --git a/k8s/xpayment-adapter-service.yml b/k8s_xx/xpayment-adapter-service.yml similarity index 100% rename from k8s/xpayment-adapter-service.yml rename to k8s_xx/xpayment-adapter-service.yml diff --git a/payment-service-app/k8s/payment-service.yml b/payment-service-app/k8s/payment-service.yml deleted file mode 100644 index ef0ee07..0000000 --- a/payment-service-app/k8s/payment-service.yml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: payment-service-app - labels: - app: payment-service-app -spec: - replicas: 1 # одно приложение (можно увеличить) - selector: - matchLabels: - app: payment-service-app - template: - metadata: - labels: - app: payment-service-app - spec: - containers: - - name: payment-service-app - image: payment-service:latest # докер образ - ports: - - containerPort: 8080 # порт внутри Pod’а - env: - - name: SPRING_DATASOURCE_URL - value: jdbc:postgresql://my-pg-postgresql.db.svc.cluster.local:5432/payment-db - - name: SPRING_DATASOURCE_USERNAME - value: postgres - - name: SPRING_DATASOURCE_PASSWORD - value: secret - - name: SPRING_KAFKA_BOOTSTRAP_SERVERS - value: my-kafka.kafka.svc.cluster.local:9092 - - name: SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI - value: http://keycloak.keycloak.svc.cluster.local:8080/realms/iprody-lms ---- -apiVersion: v1 -kind: Service -metadata: - name: payment-service-app - labels: - app: payment-service-app -spec: - selector: - app: payment-service-app - ports: - - protocol: TCP - port: 8080 # порт внутри кластера - targetPort: 8080 # порт контейнера - nodePort: 30080 # внешний порт (только если type=NodePort) - type: NodePort # вариант для доступа снаружи (Minikube, DockerDesktop) From cd4279e0e4513fd8005d8f240a2aebf39eaa2f73 Mon Sep 17 00:00:00 2001 From: alexk11 Date: Wed, 4 Feb 2026 21:49:48 +0300 Subject: [PATCH 11/12] added k8s deployment possibility --- k8s_xx/k8s-demo.yml | 39 ----------------------- k8s_xx/kafka-values.yml | 12 -------- k8s_xx/keycloak-values.yml | 30 ------------------ k8s_xx/payment-service.yml | 48 ----------------------------- k8s_xx/xpayment-adapter-service.yml | 43 -------------------------- 5 files changed, 172 deletions(-) delete mode 100644 k8s_xx/k8s-demo.yml delete mode 100644 k8s_xx/kafka-values.yml delete mode 100644 k8s_xx/keycloak-values.yml delete mode 100644 k8s_xx/payment-service.yml delete mode 100644 k8s_xx/xpayment-adapter-service.yml diff --git a/k8s_xx/k8s-demo.yml b/k8s_xx/k8s-demo.yml deleted file mode 100644 index db4f02f..0000000 --- a/k8s_xx/k8s-demo.yml +++ /dev/null @@ -1,39 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment # имя Deployment -spec: - replicas: 3 # количество реплик (Pod’ов) - selector: # селектор для идентификации Pod’ов - matchLabels: - app: nginx # выбираем Pod’ы с меткой app=nginx - template: # шаблон для создания Pod’ов - metadata: - labels: - app: nginx # метка, чтобы Pod соответствовал селектору - spec: # спецификация Pod’а - containers: # список контейнеров внутри Pod’а - - name: nginx # имя контейнера - image: nginx:1.25 # образ контейнера (nginx версии 1.25) - ports: - - containerPort: 80 # порт, который будет слушать контейнер (внутри Pod) - ---- -apiVersion: v1 -kind: Service -metadata: - name: nginx-service -spec: - selector: - app: nginx # выбираем Pod’ы с меткой app=nginx - ports: # список портов - - protocol: TCP # протокол (TCP по умолчанию) - port: 80 # порт Service - targetPort: 80 # порт внутри Pod’а - type: ClusterIP # тип Service: - -# ClusterIP (по умолчанию) -- доступ только изнутри кластера - -# NodePort -- открывает порт на нодах для внешнего доступа - -# LoadBalancer -- для облачных провайдеров, создаёт балансировщик \ No newline at end of file diff --git a/k8s_xx/kafka-values.yml b/k8s_xx/kafka-values.yml deleted file mode 100644 index 63c0c70..0000000 --- a/k8s_xx/kafka-values.yml +++ /dev/null @@ -1,12 +0,0 @@ -kraft: - enabled: true # режим KRaft (по умолчанию true в новых релизах) - -controller: - replicaCount: 1 # один контроллер для стенда - -broker: - replicaCount: 1 # один брокер - -listeners: - client: - protocol: PLAINTEXT # для простоты: без TLS/SASL на учебном стенде diff --git a/k8s_xx/keycloak-values.yml b/k8s_xx/keycloak-values.yml deleted file mode 100644 index db30dca..0000000 --- a/k8s_xx/keycloak-values.yml +++ /dev/null @@ -1,30 +0,0 @@ -image: - registry: quay.io - repository: keycloak/keycloak - tag: 24.0.3 - -extraEnvVars: - - name: KEYCLOAK_ADMIN - value: admin - - name: KEYCLOAK_ADMIN_PASSWORD - value: admin - -command: - - start-dev - - --http-port=8080 - - --import-realm - - --log-level=DEBUG - -service: - ports: - http: 8085 - -extraVolumes: - - name: realm-import - configMap: - name: keycloak-realm - -extraVolumeMounts: - - name: realm-import - mountPath: /opt/keycloak/data/import/realm-export.json - subPath: realm-export.json \ No newline at end of file diff --git a/k8s_xx/payment-service.yml b/k8s_xx/payment-service.yml deleted file mode 100644 index 6a84c4d..0000000 --- a/k8s_xx/payment-service.yml +++ /dev/null @@ -1,48 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: payment-service-app - labels: - app: payment-service-app -spec: - replicas: 1 # одно приложение (можно увеличить) - selector: - matchLabels: - app: payment-service-app - template: - metadata: - labels: - app: payment-service-app - spec: - containers: - - name: payment-service-app - image: payment-service:latest # докер образ - ports: - - containerPort: 8080 # порт внутри Pod’а - env: # переменные окружения - - name: SPRING_DATASOURCE_URL - value: jdbc:postgresql://my-pg-postgresql.db.svc.cluster.local:5432/payment-db - - name: SPRING_DATASOURCE_USERNAME - value: postgres - - name: SPRING_DATASOURCE_PASSWORD - value: secret - - name: SPRING_KAFKA_BOOTSTRAP_SERVERS - value: my-kafka.kafka.svc.cluster.local:9092 - - name: SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI - value: http://keycloak.keycloak.svc.cluster.local:8080/realms/iprody-lms ---- -apiVersion: v1 -kind: Service -metadata: - name: payment-service-app - labels: - app: payment-service-app -spec: - selector: - app: payment-service-app - ports: - - protocol: TCP - port: 8080 # порт внутри кластера - targetPort: 8080 # порт контейнера - nodePort: 30080 # внешний порт (только если type=NodePort) - type: NodePort # вариант для доступа снаружи (Minikube, DockerDesktop) \ No newline at end of file diff --git a/k8s_xx/xpayment-adapter-service.yml b/k8s_xx/xpayment-adapter-service.yml deleted file mode 100644 index c6ab644..0000000 --- a/k8s_xx/xpayment-adapter-service.yml +++ /dev/null @@ -1,43 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: xpayment-adapter-app -spec: - replicas: 1 - selector: - matchLabels: - app: x-payment-adapter-app - template: - metadata: - labels: - app: x-payment-adapter-app - spec: - containers: - - name: x-payment-adapter-app - image: x-payment-adapter-app:latest - ports: - - containerPort: 8080 - env: - - name: SPRING_KAFKA_BOOTSTRAP_SERVERS - value: "my-kafka.kafka.svc.cluster.local:9093" - - name: SPRING_KAFKA_CONSUMER_GROUP_ID - value: "xpayment-adapter-result-consumers" - - name: APP_KAFKA_TOPIC_REQUEST - value: "xpayment-adapter.requests" - - name: APP_KAFKA_TOPIC_RESPONSE - value: "xpayment-adapter.responses" - - name: RABBITMQ_URL - value: "amqp://rabbitmq.rabbitmq.svc.cluster.local:5672" ---- -apiVersion: v1 -kind: Service -metadata: - name: xpayment-adapter-app -spec: - selector: - app: x-payment-adapter-app - ports: - - protocol: TCP - port: 8080 - targetPort: 8080 - type: ClusterIP \ No newline at end of file From d203253337320ff45908ba9f6cc40f6a73d49d79 Mon Sep 17 00:00:00 2001 From: alexk11 Date: Tue, 10 Feb 2026 15:57:47 +0300 Subject: [PATCH 12/12] updated xpayment-api url --- docker-compose.yml | 2 +- payment-service-app/pom.xml | 1 - xpayment-adapter-app/src/main/resources/application.yaml | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 7a03f1a..8484a3a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: image: payment-service-app build: context: payment-service-app - dockerfile: payment-service-app/Dockerfile + dockerfile: Dockerfile ports: - "8082:8082" depends_on: diff --git a/payment-service-app/pom.xml b/payment-service-app/pom.xml index b4a06e2..effc67b 100644 --- a/payment-service-app/pom.xml +++ b/payment-service-app/pom.xml @@ -127,7 +127,6 @@ mapstruct-processor ${mapstruct.version} - diff --git a/xpayment-adapter-app/src/main/resources/application.yaml b/xpayment-adapter-app/src/main/resources/application.yaml index 3fea5fd..f32464e 100644 --- a/xpayment-adapter-app/src/main/resources/application.yaml +++ b/xpayment-adapter-app/src/main/resources/application.yaml @@ -46,7 +46,7 @@ spring: xpayment-api: client: - url: http://xpayment-api.xpayment-api-1:9999 + url: http://host.docker.internal:9999 username: paymentAgentIprody password: iprodyTestPassword0123 account: paymentAgentIprodyApiToken