diff --git a/payment-service-app/docker-compose.yml b/docker-compose.yml similarity index 71% rename from payment-service-app/docker-compose.yml rename to docker-compose.yml index 0b616b6..8484a3a 100644 --- a/payment-service-app/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: payment-service-app: image: payment-service-app build: - context: . + context: payment-service-app dockerfile: Dockerfile ports: - "8082:8082" @@ -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 @@ -57,11 +69,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: @@ -98,6 +110,25 @@ services: depends_on: - kafka + rabbitmq: + image: rabbitmq:4.2.3-management-alpine + 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: + postgres_data: pgdata: kafka_data: + rabbitmq_data: \ No newline at end of file diff --git a/k8s/k8s-nginx-values.yml b/k8s/k8s-nginx-values.yml new file mode 100644 index 0000000..cfb085d --- /dev/null +++ b/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/k8s/kafka-values.yml b/k8s/kafka-values.yml new file mode 100644 index 0000000..77ec8e3 --- /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 на учебном стенде \ No newline at end of file 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-values.yml b/k8s/payment-service-values.yml new file mode 100644 index 0000000..df8710e --- /dev/null +++ b/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/k8s/xpayment-adapter-values.yml b/k8s/xpayment-adapter-values.yml new file mode 100644 index 0000000..9b27872 --- /dev/null +++ b/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/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..effc67b 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 @@ -127,7 +127,6 @@ mapstruct-processor ${mapstruct.version} - 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-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..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: @@ -13,14 +15,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..600bd85 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ com.iprody - PaymentService + Payment-Service-Main 1.0-SNAPSHOT pom @@ -36,6 +36,7 @@ 3.6.0 1.6.3 1.21.4 + 3.5.8 0.0.1-SNAPSHOT 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 0000000..8ba5d78 Binary files /dev/null and b/rabbitmq_delayed_message_exchange-4.2.0.ez differ 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 1e05034..24d39ed 100644 --- a/xpayment-adapter-app/pom.xml +++ b/xpayment-adapter-app/pom.xml @@ -4,15 +4,17 @@ 4.0.0 com.iprody - PaymentService + Payment-Service-Main 1.0-SNAPSHOT ../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 @@ -44,6 +46,11 @@ org.springframework.kafka spring-kafka + + org.springframework.boot + spring-boot-starter-amqp + 3.5.8 + com.iprody payment-service-api @@ -55,6 +62,12 @@ ${lombok.version} true + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.21.0 + compile + org.openapitools jackson-databind-nullable @@ -71,6 +84,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 + @@ -94,6 +127,20 @@ + + maven-assembly-plugin + + + + com.iprody.adapter.XPaymentAdapterAppApplication + + + + jar-with-dependencies + + false + + org.springframework.boot spring-boot-maven-plugin @@ -106,6 +153,13 @@ + + + + repackage + + + org.openapitools 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..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 @@ -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; @@ -16,19 +18,21 @@ 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, + 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 = converter.toCreateChargeRequest(dto); + ChargeResponse response = defaultApi.createCharge(chargeRequest); + return converter.toCreateChargeResponseDto(response); } catch (Exception e) { throw toRestClientException("POST /charges failed", e); } @@ -37,7 +41,7 @@ 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/async/handler/RequestMessageHandler.java b/xpayment-adapter-app/src/main/java/com/iprody/adapter/async/handler/RequestMessageHandler.java index 43be2a4..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 @@ -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 {}", message.getPaymentGuid(), 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/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/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..b535394 --- /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("${spring.app.rabbitmq.max-retries:60}") + private int maxRetries; + + @Value("${spring.app.rabbitmq.interval-ms:60000}") + private long intervalMs; + + @Autowired + public PaymentStateCheckListener( + RabbitTemplate rabbitTemplate, + @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; + this.routingKey = routingKey; + this.dlxExchangeName = dlxExchangeName; + this.dlxRoutingKey = dlxRoutingKey; + this.paymentStatusCheckHandler = paymentStatusCheckHandler; + } + + @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); + 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..432813e --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/PaymentStateCheckRegistrarImpl.java @@ -0,0 +1,64 @@ +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; +import org.springframework.stereotype.Service; +import java.math.BigDecimal; +import java.util.UUID; + + +@Service +@Slf4j +public class PaymentStateCheckRegistrarImpl implements PaymentStateCheckRegistrar { + + private final RabbitTemplate rabbitTemplate; + private final String exchangeName; + private final String routingKey; + + @Value("${spring.app.rabbitmq.max-retries:60}") + private int maxRetries; + + @Value("${spring.app.rabbitmq.interval-ms:60000}") + private long intervalMs; + + @Autowired + public PaymentStateCheckRegistrarImpl( + RabbitTemplate rabbitTemplate, + @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; + } + + @Override + public void register( + UUID chargeGuid, + UUID paymentGuid, + BigDecimal amount, + String currency) { + + log.info("Registering message with RabbitMQ, id={}", paymentGuid); + + 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..59bebd8 --- /dev/null +++ b/xpayment-adapter-app/src/main/java/com/iprody/adapter/checkstate/RabbitMqPaymentRetryConfig.java @@ -0,0 +1,54 @@ +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; +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("${spring.app.rabbitmq.queue-name}") + private String queueName; + + @Value("${spring.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(); + } + + @Bean + public MessageConverter messageConverter() { + return new Jackson2JsonMessageConverter(); + } + +} 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/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 2aaedff..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,10 +1,15 @@ package com.iprody.adapter.dto; +import lombok.Getter; +import lombok.Setter; + import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; import java.util.UUID; +@Getter +@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..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,10 +1,15 @@ package com.iprody.adapter.dto; +import lombok.Builder; +import lombok.Getter; + import java.math.BigDecimal; import java.util.HashMap; import java.util.Map; 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..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 @@ -2,18 +2,26 @@ 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); - CreateChargeResponseDto toCreateChargeResponseDto(ChargeResponse chargeResponse); CreateChargeRequest toCreateChargeRequest(CreateChargeRequestDto dto); - ChargeResponse toChargeResponse(CreateChargeResponseDto dto); + @Mapping(target = "paymentGuid", source = "order") + @Mapping(target = "transactionRefId", source = "id") + @Mapping(target = "occurredAt", expression = "java(java.time.OffsetDateTime.now())") + XPaymentAdapterResponseMessage responseDtoToKafkaMessage(CreateChargeResponseDto response); + + @Mapping(target = "order", source = "paymentGuid") + CreateChargeRequestDto kafkaMessageToRequestDto(XPaymentAdapterRequestMessage request); } 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..4b1d118 --- /dev/null +++ b/xpayment-adapter-app/src/main/resources/README.txt @@ -0,0 +1,9 @@ +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 . + +K8S: + + helm install keycloak bitnami/keycloak -f keycloak-values.yml --set global.security.allowInsecureImages=true \ No newline at end of file diff --git a/xpayment-adapter-app/src/main/resources/application-kafka.yaml b/xpayment-adapter-app/src/main/resources/application-kafka.yaml index 14a809b..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). @@ -32,10 +32,3 @@ spring: xpayment-adapter: 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 new file mode 100644 index 0000000..93dd106 --- /dev/null +++ b/xpayment-adapter-app/src/main/resources/application-rabbitmq.yaml @@ -0,0 +1,17 @@ +spring: + + rabbitmq: + host: rabbitmq + port: 5672 + username: admin + password: admin + + 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 diff --git a/xpayment-adapter-app/src/main/resources/application.yaml b/xpayment-adapter-app/src/main/resources/application.yaml index 303d059..f32464e 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 + active: kafka,rabbitmq security: oauth2: resourceserver: @@ -40,4 +42,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://host.docker.internal:9999 + username: paymentAgentIprody + password: iprodyTestPassword0123 + account: paymentAgentIprodyApiToken diff --git a/xpayment-api.openapi.yml b/xpayment-api.openapi.yml deleted file mode 100644 index b327aad..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