fixes, add already_exist_created flow, add GetDispute handler (#9)

This commit is contained in:
Anatolii Karlov 2024-09-19 21:09:35 +07:00 committed by GitHub
parent 7c61521a9a
commit 6faacd9ac1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
44 changed files with 633 additions and 204 deletions

View File

@ -4,6 +4,8 @@ on:
push:
branches:
- 'master'
- 'main'
- 'epic/**'
jobs:
build-and-deploy:

View File

@ -115,15 +115,15 @@
- Перед переводом диспута в финальный статус саппорт должен будет забиндить айди созданного диспута в провайдере через
ручку `BindCreated()`. Здесь особенность, что этот метод фильтрует возможность биндить диспуты только созданные
вручную (из `manual_parsing_created`)
вручную (из `manual_created`)
Далее, в режиме ручного разбора есть опция финализации диспута в фейл (`CancelPending()`) либо в
успех (`ApprovePending()`). Здесь особенность, что в фейл можно перевести любой диспут имеющий не финальный статус, а в
успех можно перевести, только если гарантировано создан внешний диспут у провайдера (
из `pending`,`manual_parsing_binded_pending`)
из `pending`,`manual_pending`)
- Из за того, что для ручных диспутов добавлены отдельные
статусы `manual_parsing_binded_pending` ,`manual_parsing_created` не происходит ситуации, что такие диспуты попадут в
статусы `manual_pending` ,`manual_created` не происходит ситуации, что такие диспуты попадут в
таску `PendingDisputesService` которая автоматически вызывает апи провайдера для проверки статуса
# Схема аппрува корректировок

10
pom.xml
View File

@ -47,7 +47,7 @@
<dependency>
<groupId>dev.vality</groupId>
<artifactId>provider-disputes-proto</artifactId>
<version>1.16-61eff7c</version>
<version>1.18-6602d79</version>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
@ -89,10 +89,6 @@
</dependency>
<!--spring-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
@ -193,6 +189,10 @@
<version>3.0.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>

View File

@ -4,8 +4,10 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableAsync
@EnableScheduling
@ServletComponentScan
@SpringBootApplication(scanBasePackages = {"dev.vality.disputes", "dev.vality.swag"})
public class DisputesApiApplication extends SpringApplication {

View File

@ -23,7 +23,7 @@ public class Status200ResponseConverter {
private Status200Response.StatusEnum getStatus(Dispute dispute) {
return switch (dispute.getStatus()) {
case created, pending, manual_created, manual_pending, create_adjustment ->
case created, pending, manual_created, manual_pending, create_adjustment, already_exist_created ->
Status200Response.StatusEnum.PENDING;
case succeeded -> Status200Response.StatusEnum.SUCCEEDED;
case cancelled, failed -> Status200Response.StatusEnum.FAILED;

View File

@ -1,15 +1,13 @@
package dev.vality.disputes.api.model;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.Data;
import lombok.ToString;
import java.util.Map;
@Builder
@Getter
@Setter
@Data
public class PaymentParams {
private String invoiceId;

View File

@ -4,7 +4,6 @@ import dev.vality.disputes.dao.FileMetaDao;
import dev.vality.disputes.domain.tables.pojos.FileMeta;
import dev.vality.disputes.service.external.FileStorageService;
import dev.vality.swag.disputes.model.CreateRequest;
import dev.vality.swag.disputes.model.CreateRequestAttachmentsInner;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
@ -24,13 +23,13 @@ public class ApiAttachmentsService {
@Transactional(propagation = Propagation.REQUIRED)
public void createAttachments(CreateRequest req, Long disputeId) {
log.debug("Trying to save Attachments {}", disputeId);
for (CreateRequestAttachmentsInner attachment : req.getAttachments()) {
for (var attachment : req.getAttachments()) {
// validate
MediaType.valueOf(attachment.getMimeType());
// http 500
var fileId = fileStorageService.saveFile(attachment.getData());
var fileMeta = new FileMeta(fileId, disputeId, attachment.getMimeType());
log.debug("Trying to save Attachment {}", fileMeta.getFileId());
log.debug("Trying to save Attachment {}", fileMeta);
// http 500
fileMetaDao.save(fileMeta);
}

View File

@ -74,6 +74,7 @@ public class ApiDisputesService {
DisputeStatus.pending,
DisputeStatus.manual_created,
DisputeStatus.manual_pending,
DisputeStatus.create_adjustment);
DisputeStatus.create_adjustment,
DisputeStatus.already_exist_created);
}
}

View File

@ -5,20 +5,21 @@ import dev.vality.damsel.domain.TransactionInfo;
import dev.vality.damsel.payment_processing.InvoicePayment;
import dev.vality.disputes.api.model.PaymentParams;
import dev.vality.disputes.security.AccessData;
import dev.vality.disputes.service.external.DominantService;
import dev.vality.disputes.service.external.impl.dominant.DominantAsyncService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentParamsBuilder {
private final DominantService dominantService;
private final DominantAsyncService dominantAsyncService;
@SneakyThrows
public PaymentParams buildGeneralPaymentContext(AccessData accessData) {
@ -26,28 +27,42 @@ public class PaymentParamsBuilder {
log.debug("Start building PaymentParams id={}", invoice.getInvoice().getId());
var payment = accessData.getPayment();
// http 500
var terminal = dominantService.getTerminal(payment.getRoute().getTerminal());
var terminal = dominantAsyncService.getTerminal(payment.getRoute().getTerminal());
var currency = Optional.of(payment)
.filter(p -> p.getPayment().isSetCost())
.map(p -> p.getPayment().getCost())
// http 500
.map(cost -> dominantService.getCurrency(cost.getCurrency()));
.map(cost -> dominantAsyncService.getCurrency(cost.getCurrency()));
var paymentParams = PaymentParams.builder()
.invoiceId(invoice.getInvoice().getId())
.paymentId(payment.getPayment().getId())
.terminalId(payment.getRoute().getTerminal().getId())
.providerId(payment.getRoute().getProvider().getId())
.providerTrxId(getProviderTrxId(payment))
.currencyName(currency.map(Currency::getName).orElse(null))
.currencySymbolicCode(currency.map(Currency::getSymbolicCode).orElse(null))
.currencyNumericCode(currency.map(Currency::getNumericCode).map(Short::intValue).orElse(null))
.currencyExponent(currency.map(Currency::getExponent).map(Short::intValue).orElse(null))
.currencyName(getCurrency(currency)
.map(Currency::getName).orElse(null))
.currencySymbolicCode(getCurrency(currency)
.map(Currency::getSymbolicCode).orElse(null))
.currencyNumericCode(getCurrency(currency)
.map(Currency::getNumericCode).map(Short::intValue).orElse(null))
.currencyExponent(getCurrency(currency)
.map(Currency::getExponent).map(Short::intValue).orElse(null))
.options(terminal.get().getOptions())
.build();
log.debug("Finish building PaymentParams {}", paymentParams);
return paymentParams;
}
private Optional<Currency> getCurrency(Optional<CompletableFuture<Currency>> currency) {
return currency.map(currencyCompletableFuture -> {
try {
return currencyCompletableFuture.get();
} catch (Throwable e) {
throw new RuntimeException(e);
}
});
}
private String getProviderTrxId(InvoicePayment payment) {
return Optional.ofNullable(payment.getLastTransactionInfo())
.map(TransactionInfo::getId)

View File

@ -7,8 +7,6 @@ import dev.vality.damsel.payment_processing.InvoicingSrv;
import dev.vality.file.storage.FileStorageSrv;
import dev.vality.token.keeper.TokenAuthenticatorSrv;
import dev.vality.woody.thrift.impl.http.THSpawnClientBuilder;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -71,11 +69,6 @@ public class ApplicationConfig {
.build(FileStorageSrv.Iface.class);
}
@Bean
public CloseableHttpClient httpClient() {
return HttpClients.createDefault();
}
@Bean
public ExecutorService disputesThreadPool(@Value("${dispute.batchSize}") int threadPoolSize) {
final var threadFactory = new ThreadFactoryBuilder()

View File

@ -11,11 +11,12 @@ import java.util.concurrent.Executor;
@SuppressWarnings("AbbreviationAsWordInName")
public class AsyncMDCConfiguration {
@Bean
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
@Bean("dominantAsyncServiceExecutor")
public Executor dominantAsyncServiceExecutor() {
var executor = new ThreadPoolTaskExecutor();
executor.setTaskDecorator(new MDCTaskDecorator());
executor.initialize();
executor.setThreadNamePrefix("dominantAsyncService-thread-");
return executor;
}
}

View File

@ -0,0 +1,73 @@
package dev.vality.disputes.config;
import dev.vality.disputes.config.properties.HttpClientProperties;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.hc.client5.http.config.ConnectionConfig;
import org.apache.hc.client5.http.config.RequestConfig;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.TrustAllStrategy;
import org.apache.hc.core5.http.config.Registry;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.util.Timeout;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class HttpClientConfig {
private final HttpClientProperties httpClientProperties;
@Bean
public CloseableHttpClient httpClient(
PoolingHttpClientConnectionManager manager,
RequestConfig requestConfig) {
return HttpClients.custom()
.setConnectionManager(manager)
.setDefaultRequestConfig(requestConfig)
.disableAutomaticRetries()
.setConnectionManagerShared(true)
.build();
}
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
var connectionManager = new PoolingHttpClientConnectionManager(connectionSocketFactory());
connectionManager.setMaxTotal(httpClientProperties.getMaxTotalPooling());
connectionManager.setDefaultMaxPerRoute(httpClientProperties.getDefaultMaxPerRoute());
connectionManager.setDefaultConnectionConfig(connectionConfig(httpClientProperties));
return connectionManager;
}
@Bean
public RequestConfig requestConfig() {
return RequestConfig.custom()
.setConnectionRequestTimeout(Timeout.ofMilliseconds(httpClientProperties.getPoolTimeout()))
.build();
}
@SneakyThrows
private Registry<ConnectionSocketFactory> connectionSocketFactory() {
var sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustAllStrategy()).build();
var sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
return RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslConnectionSocketFactory)
.register("http", new PlainConnectionSocketFactory())
.build();
}
private ConnectionConfig connectionConfig(HttpClientProperties httpClientProperties) {
return ConnectionConfig.custom()
.setConnectTimeout(Timeout.ofMilliseconds(httpClientProperties.getConnectionTimeout()))
.setSocketTimeout(Timeout.ofMilliseconds(httpClientProperties.getRequestTimeout()))
.build();
}
}

View File

@ -0,0 +1,28 @@
package dev.vality.disputes.config.properties;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.annotation.Validated;
@Getter
@Setter
@Validated
@Configuration
@ConfigurationProperties("http-client")
public class HttpClientProperties {
@NotNull
private int maxTotalPooling;
@NotNull
private int defaultMaxPerRoute;
@NotNull
private int requestTimeout;
@NotNull
private int poolTimeout;
@NotNull
private int connectionTimeout;
}

View File

@ -62,15 +62,18 @@ public class DisputeDao extends AbstractGenericDao {
.orElse(List.of());
}
public Dispute getForUpdateSkipLocked(long disputeId) {
public Optional<Dispute> get(long disputeId) {
var query = getDslContext().selectFrom(DISPUTE)
.where(DISPUTE.ID.eq(disputeId));
return Optional.ofNullable(fetchOne(query, disputeRowMapper));
}
public Optional<Dispute> getForUpdateSkipLocked(long disputeId) {
var query = getDslContext().selectFrom(DISPUTE)
.where(DISPUTE.ID.eq(disputeId))
.forUpdate()
.skipLocked();
return Optional.ofNullable(fetchOne(query, disputeRowMapper))
.orElseThrow(
() -> new NotFoundException(
String.format("Dispute not found, disputeId='%s'", disputeId)));
return Optional.ofNullable(fetchOne(query, disputeRowMapper));
}
public List<Dispute> getDisputesForUpdateSkipLocked(int limit, DisputeStatus disputeStatus) {

View File

@ -0,0 +1,128 @@
package dev.vality.disputes.manualparsing;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import dev.vality.disputes.admin.*;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.Base64;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping({"/debug/disputes-api/manual-parsing"})
@Slf4j
public class DebugManualParsingController {
private final ManualParsingServiceSrv.Iface manualParsingHandler;
private final ObjectMapper objectMapper = new ObjectMapper();
@PostMapping("/cancel")
@SneakyThrows
public void cancelPending(@RequestBody String body) {
log.debug("cancelPending {}", body);
manualParsingHandler.cancelPending(objectMapper.readValue(body, CancelParamsRequest.class));
}
@PostMapping("/approve")
@SneakyThrows
public void approvePending(@RequestBody String body) {
log.debug("approvePending {}", body);
manualParsingHandler.approvePending(objectMapper.readValue(body, ApproveParamsRequest.class));
}
@PostMapping("/bind")
@SneakyThrows
public void bindCreated(@RequestBody String body) {
log.debug("bindCreated {}", body);
manualParsingHandler.bindCreated(objectMapper.readValue(body, BindParamsRequest.class));
}
@GetMapping("/get")
@SneakyThrows
public DisputeResult getDisputes(@RequestBody String body) {
log.debug("getDispute {}", body);
var dispute = manualParsingHandler.getDispute(objectMapper.readValue(body, DisputeParamsRequest.class));
return objectMapper.convertValue(dispute, new TypeReference<>() {
});
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class DisputeResult {
@JsonProperty("disputes")
private List<Dispute> disputes;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Dispute {
@JsonProperty("disputeId")
private String disputeId; // required
@JsonProperty("providerDisputeId")
private String providerDisputeId; // optional
@JsonProperty("invoiceId")
private String invoiceId; // required
@JsonProperty("paymentId")
private String paymentId; // required
@JsonProperty("providerTrxId")
private String providerTrxId; // required
@JsonProperty("status")
private String status; // required
@JsonProperty("errorMessage")
private String errorMessage; // optional
@JsonProperty("amount")
private String amount; // required
@JsonProperty("changedAmount")
private String changedAmount; // optional
@JsonProperty("skipCallHgForCreateAdjustment")
private boolean skipCallHgForCreateAdjustment; // required
@JsonProperty("attachments")
@JsonDeserialize(using = AttachmentDeserializer.class)
public List<Attachment> attachments;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Attachment {
private String data;
}
public static class AttachmentDeserializer extends JsonDeserializer<Attachment> {
private final ObjectMapper mapper = new ObjectMapper();
@Override
public Attachment deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException {
var node = (JsonNode) parser.getCodec().readTree(parser);
if (node.isObject()) {
var attachment = mapper.convertValue(node, new TypeReference<dev.vality.disputes.admin.Attachment>() {
});
var attachmentResult = new Attachment();
attachmentResult.setData(Base64.getEncoder().encodeToString(attachment.getData()));
return attachmentResult;
}
return null;
}
}
}

View File

@ -1,19 +1,30 @@
package dev.vality.disputes.manualparsing;
import dev.vality.disputes.admin.ApproveParams;
import dev.vality.disputes.admin.BindParams;
import dev.vality.disputes.admin.CancelParams;
import dev.vality.disputes.admin.*;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.dao.FileMetaDao;
import dev.vality.disputes.dao.ProviderDisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.domain.tables.pojos.ProviderDispute;
import dev.vality.disputes.service.external.FileStorageService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Optional;
import static dev.vality.disputes.api.service.ApiDisputesService.DISPUTE_PENDING;
@Slf4j
@ -24,13 +35,20 @@ public class ManualParsingDisputesService {
private final DisputeDao disputeDao;
private final ProviderDisputeDao providerDisputeDao;
private final FileMetaDao fileMetaDao;
private final FileStorageService fileStorageService;
private final CloseableHttpClient httpClient;
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public void cancelPendingDispute(CancelParams cancelParams) {
var disputeId = cancelParams.getDisputeId();
var cancelReason = cancelParams.getCancelReason().orElse(null);
log.debug("Trying to getForUpdateSkipLocked {}", disputeId);
var dispute = disputeDao.getForUpdateSkipLocked(Long.parseLong(disputeId));
var disputeOptional = disputeDao.getForUpdateSkipLocked(Long.parseLong(disputeId));
if (disputeOptional.isEmpty()) {
return;
}
var cancelReason = cancelParams.getCancelReason().orElse(null);
var dispute = disputeOptional.get();
log.debug("GetForUpdateSkipLocked has been found {}", dispute);
if (DISPUTE_PENDING.contains(dispute.getStatus())) {
// используется не failed, а cancelled чтоб можно было понять, что зафейлен по внешнему вызову
@ -46,7 +64,11 @@ public class ManualParsingDisputesService {
public void approvePendingDispute(ApproveParams approveParam) {
var disputeId = approveParam.getDisputeId();
log.debug("Trying to getForUpdateSkipLocked {}", disputeId);
var dispute = disputeDao.getForUpdateSkipLocked(Long.parseLong(disputeId));
var disputeOptional = disputeDao.getForUpdateSkipLocked(Long.parseLong(disputeId));
if (disputeOptional.isEmpty()) {
return;
}
var dispute = disputeOptional.get();
log.debug("GetForUpdateSkipLocked has been found {}", dispute);
var skipCallHg = approveParam.isSkipCallHgForCreateAdjustment();
var targetStatus = skipCallHg ? DisputeStatus.succeeded : DisputeStatus.create_adjustment;
@ -70,19 +92,73 @@ public class ManualParsingDisputesService {
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public void bindCreatedDispute(BindParams bindParam) {
var disputeId = bindParam.getDisputeId();
var providerDisputeId = bindParam.getProviderDisputeId();
log.debug("Trying to getForUpdateSkipLocked {}", disputeId);
var dispute = disputeDao.getForUpdateSkipLocked(Long.parseLong(disputeId));
var disputeOptional = disputeDao.getForUpdateSkipLocked(Long.parseLong(disputeId));
if (disputeOptional.isEmpty()) {
return;
}
var providerDisputeId = bindParam.getProviderDisputeId();
var dispute = disputeOptional.get();
log.debug("GetForUpdateSkipLocked has been found {}", dispute);
if (dispute.getStatus() == DisputeStatus.manual_created) {
// обрабатываем здесь только вручную созданные диспуты, у остальных предполагается,
// что providerDisputeId будет сохранен после создания диспута по API провайдера
log.info("Trying to set manual_parsing_binded_pending Dispute status {}", dispute);
log.info("Trying to set manual_pending Dispute status {}", dispute);
providerDisputeDao.save(new ProviderDispute(providerDisputeId, dispute.getId()));
disputeDao.update(dispute.getId(), DisputeStatus.manual_pending);
log.debug("Dispute status has been set to manual_parsing_binded_pending {}", dispute);
log.debug("Dispute status has been set to manual_pending {}", dispute);
} else if (dispute.getStatus() == DisputeStatus.already_exist_created) {
log.info("Trying to set pending Dispute status {}", dispute);
providerDisputeDao.save(new ProviderDispute(providerDisputeId, dispute.getId()));
disputeDao.update(dispute.getId(), DisputeStatus.pending);
log.debug("Dispute status has been set to pending {}", dispute);
} else {
log.info("Request was skipped by inappropriate status {}", dispute);
}
}
@SneakyThrows
public Dispute getDispute(DisputeParams disputeParams) {
var disputeId = disputeParams.getDisputeId();
var disputeOptional = disputeDao.get(Long.parseLong(disputeId));
if (disputeOptional.isEmpty()) {
log.debug("Trying to get Dispute but null {}", disputeId);
return null;
}
var dispute = disputeOptional.get();
var disputeResult = new Dispute();
disputeResult.setDisputeId(disputeId);
disputeResult.setProviderDisputeId(Optional.ofNullable(providerDisputeDao.get(dispute.getId()))
.map(ProviderDispute::getProviderDisputeId)
.orElse(null));
disputeResult.setInvoiceId(dispute.getInvoiceId());
disputeResult.setPaymentId(dispute.getPaymentId());
disputeResult.setProviderTrxId(dispute.getProviderTrxId());
disputeResult.setStatus(dispute.getStatus().name());
disputeResult.setErrorMessage(dispute.getErrorMessage());
disputeResult.setAmount(String.valueOf(dispute.getAmount()));
disputeResult.setChangedAmount(Optional.ofNullable(dispute.getChangedAmount())
.map(String::valueOf)
.orElse(null));
disputeResult.setSkipCallHgForCreateAdjustment(dispute.getSkipCallHgForCreateAdjustment());
log.debug("Dispute getDispute {}", disputeResult);
var disputeFiles = fileMetaDao.getDisputeFiles(dispute.getId());
if (disputeFiles == null) {
return disputeResult;
}
disputeResult.setAttachments(new ArrayList<>());
for (var disputeFile : disputeFiles) {
var downloadUrl = fileStorageService.generateDownloadUrl(disputeFile.getFileId());
var data = httpClient.execute(
new HttpGet(downloadUrl),
new AbstractHttpClientResponseHandler<byte[]>() {
@Override
public byte[] handleEntity(HttpEntity entity) throws IOException {
return EntityUtils.toByteArray(entity);
}
});
disputeResult.getAttachments().get().add(new Attachment(ByteBuffer.wrap(data)));
}
return disputeResult;
}
}

View File

@ -6,6 +6,8 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.thrift.TException;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
@Service
@RequiredArgsConstructor
@Slf4j
@ -30,8 +32,20 @@ public class ManualParsingHandler implements ManualParsingServiceSrv.Iface {
@Override
public void bindCreated(BindParamsRequest bindParamsRequest) throws TException {
for (BindParams bindParam : bindParamsRequest.getBindParams()) {
for (var bindParam : bindParamsRequest.getBindParams()) {
manualParsingDisputesService.bindCreatedDispute(bindParam);
}
}
@Override
public DisputeResult getDispute(DisputeParamsRequest disputeParamsRequest) throws TException {
var disputeResult = new DisputeResult(new ArrayList<>());
for (var disputeParams : disputeParamsRequest.getDisputeParams()) {
var dispute = manualParsingDisputesService.getDispute(disputeParams);
if (dispute != null) {
disputeResult.getDisputes().add(dispute);
}
}
return disputeResult;
}
}

View File

@ -8,8 +8,6 @@ import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@ -23,8 +21,7 @@ public class ManualParsingTopic {
@Value("${manual-parsing-topic.enabled}")
private boolean enabled;
@Transactional(propagation = Propagation.REQUIRED)
public void sendCreated(Dispute dispute, List<Attachment> attachments) {
public void sendCreated(Dispute dispute, List<Attachment> attachments, DisputeStatus disputeStatus) {
if (!enabled) {
return;
}
@ -32,7 +29,7 @@ public class ManualParsingTopic {
contextMap.put("dispute_id", dispute.getId().toString());
var attachmentsCollect = attachments.stream().map(Attachment::toString).collect(Collectors.joining(", "));
contextMap.put("dispute_attachments", attachmentsCollect);
contextMap.put("dispute_status", DisputeStatus.manual_created.name());
contextMap.put("dispute_status", disputeStatus.name());
MDC.setContextMap(contextMap);
log.warn("Manual parsing case");
MDC.clear();

View File

@ -6,16 +6,12 @@ import dev.vality.damsel.domain.Terminal;
import dev.vality.damsel.domain.TerminalRef;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.service.external.DominantService;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
@Service
public class ExponentialBackOffPollingServiceWrapper {
@ -33,20 +29,17 @@ public class ExponentialBackOffPollingServiceWrapper {
return getLocalDateTime(pollingInfo.getStartDateTimePolling().plusSeconds(seconds));
}
@Transactional(propagation = Propagation.REQUIRED)
@SneakyThrows
public LocalDateTime prepareNextPollingInterval(Dispute dispute) {
var pollingInfo = new PollingInfo();
var startDateTimePolling = dispute.getCreatedAt().toInstant(ZoneOffset.UTC);
pollingInfo.setStartDateTimePolling(startDateTimePolling);
pollingInfo.setMaxDateTimePolling(dispute.getPollingBefore().toInstant(ZoneOffset.UTC));
var terminal = getTerminal(dispute.getTerminalId());
var seconds = exponentialBackOffPollingService.prepareNextPollingInterval(
pollingInfo, terminal.get().getOptions());
var seconds = exponentialBackOffPollingService.prepareNextPollingInterval(pollingInfo, terminal.getOptions());
return getLocalDateTime(startDateTimePolling.plusSeconds(seconds));
}
private CompletableFuture<Terminal> getTerminal(Integer terminalId) {
private Terminal getTerminal(Integer terminalId) {
return dominantService.getTerminal(new TerminalRef(terminalId));
}

View File

@ -16,6 +16,7 @@ import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
public class TaskCreateAdjustmentsService {
private final ExecutorService disputesThreadPool;
@ -25,12 +26,12 @@ public class TaskCreateAdjustmentsService {
@Value("${dispute.isScheduleCreateAdjustmentsEnabled}")
private boolean isScheduleCreateAdjustmentsEnabled;
@Scheduled(fixedDelayString = "${dispute.fixedDelayCreateAdjustments}")
@Scheduled(fixedDelayString = "${dispute.fixedDelayCreateAdjustments}", initialDelayString = "${dispute.initialDelayCreateAdjustments}")
public void processPending() {
if (!isScheduleCreateAdjustmentsEnabled) {
return;
}
log.info("Processing create adjustments get started");
log.debug("Processing create adjustments get started");
try {
var disputes = createAdjustmentsService.getDisputesForHgCall(batchSize);
var callables = disputes.stream()
@ -40,7 +41,7 @@ public class TaskCreateAdjustmentsService {
} catch (InterruptedException ex) {
log.error("Received InterruptedException while thread executed report", ex);
Thread.currentThread().interrupt();
} catch (Exception ex) {
} catch (Throwable ex) {
log.error("Received exception while scheduler processed create adjustments", ex);
}
log.info("Create adjustments were processed");

View File

@ -25,12 +25,12 @@ public class TaskCreatedDisputesService {
@Value("${dispute.isScheduleCreatedEnabled}")
private boolean isScheduleCreatedEnabled;
@Scheduled(fixedDelayString = "${dispute.fixedDelayCreated}")
@Scheduled(fixedDelayString = "${dispute.fixedDelayCreated}", initialDelayString = "${dispute.initialDelayCreated}")
public void processCreated() {
if (!isScheduleCreatedEnabled) {
return;
}
log.info("Processing created disputes get started");
log.debug("Processing created disputes get started");
try {
var disputes = createdDisputesService.getCreatedDisputesForUpdateSkipLocked(batchSize);
var callables = disputes.stream()
@ -40,7 +40,7 @@ public class TaskCreatedDisputesService {
} catch (InterruptedException ex) {
log.error("Received InterruptedException while thread executed report", ex);
Thread.currentThread().interrupt();
} catch (Exception ex) {
} catch (Throwable ex) {
log.error("Received exception while scheduler processed created disputes", ex);
}
log.info("Created disputes were processed");

View File

@ -25,12 +25,12 @@ public class TaskPendingDisputesService {
@Value("${dispute.isSchedulePendingEnabled}")
private boolean isSchedulePendingEnabled;
@Scheduled(fixedDelayString = "${dispute.fixedDelayPending}")
@Scheduled(fixedDelayString = "${dispute.fixedDelayPending}", initialDelayString = "${dispute.initialDelayPending}")
public void processPending() {
if (!isSchedulePendingEnabled) {
return;
}
log.info("Processing pending disputes get started");
log.debug("Processing pending disputes get started");
try {
var disputes = pendingDisputesService.getPendingDisputesForUpdateSkipLocked(batchSize);
var callables = disputes.stream()
@ -40,7 +40,7 @@ public class TaskPendingDisputesService {
} catch (InterruptedException ex) {
log.error("Received InterruptedException while thread executed report", ex);
Thread.currentThread().interrupt();
} catch (Exception ex) {
} catch (Throwable ex) {
log.error("Received exception while scheduler processed pending disputes", ex);
}
log.info("Pending disputes were processed");

View File

@ -17,11 +17,8 @@ import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
@ -34,37 +31,42 @@ public class RemoteClient {
private final DisputeContextConverter disputeContextConverter;
private final DisputeParamsConverter disputeParamsConverter;
@Transactional(propagation = Propagation.REQUIRED)
@SneakyThrows
public DisputeCreatedResult createDispute(Dispute dispute, List<Attachment> attachments) {
log.debug("Trying to call dominant for RemoteClient {}", dispute.getId());
var terminal = getTerminal(dispute.getTerminalId());
var proxy = getProxy(dispute.getProviderId());
var disputeParams = disputeParamsConverter.convert(dispute, attachments, terminal.get().getOptions());
var remoteClient = providerIfaceBuilder.build(terminal.get().getOptions(), proxy.get().getUrl());
log.info("Trying to routed remote provider's createDispute() call {}", dispute);
log.debug("Trying to build disputeParams {}", dispute.getId());
var disputeParams = disputeParamsConverter.convert(dispute, attachments, terminal.getOptions());
log.debug("Trying to call ProviderIfaceBuilder {}", dispute.getId());
var remoteClient = providerIfaceBuilder.buildTHSpawnClient(terminal.getOptions(), proxy.getUrl());
log.debug("Trying to routed remote provider's createDispute() call {}", dispute.getId());
var result = remoteClient.createDispute(disputeParams);
log.debug("Routed remote provider's createDispute() has been called {}", dispute);
log.info("Routed remote provider's createDispute() has been called {} {}", dispute.getId(), result);
return result;
}
@Transactional(propagation = Propagation.REQUIRED)
@SneakyThrows
public DisputeStatusResult checkDisputeStatus(Dispute dispute, ProviderDispute providerDispute) {
log.debug("Trying to call dominant for RemoteClient {}", dispute.getId());
var terminal = getTerminal(dispute.getTerminalId());
var proxy = getProxy(dispute.getProviderId());
var disputeContext = disputeContextConverter.convert(dispute, providerDispute, terminal.get().getOptions());
var remoteClient = providerIfaceBuilder.build(terminal.get().getOptions(), proxy.get().getUrl());
log.info("Trying to routed remote provider's checkDisputeStatus() call {}", dispute);
log.debug("Trying to build disputeContext {}", dispute.getId());
var disputeContext = disputeContextConverter.convert(dispute, providerDispute, terminal.getOptions());
log.debug("Trying to call ProviderIfaceBuilder {}", dispute.getId());
var remoteClient = providerIfaceBuilder.buildTHSpawnClient(terminal.getOptions(), proxy.getUrl());
log.debug("Trying to routed remote provider's checkDisputeStatus() call {}", dispute.getId());
var result = remoteClient.checkDisputeStatus(disputeContext);
log.debug("Routed remote provider's checkDisputeStatus() has been called {}", dispute);
log.info("Routed remote provider's checkDisputeStatus() has been called {} {}", dispute.getId(), result);
return result;
}
private CompletableFuture<ProxyDefinition> getProxy(Integer providerId) {
return dominantService.getProxy(new ProviderRef(providerId));
private ProxyDefinition getProxy(Integer providerId) {
var provider = dominantService.getProvider(new ProviderRef(providerId));
return dominantService.getProxy(provider.getProxy().getRef());
}
private CompletableFuture<Terminal> getTerminal(Integer terminalId) {
private Terminal getTerminal(Integer terminalId) {
return dominantService.getTerminal(new TerminalRef(terminalId));
}
}

View File

@ -3,8 +3,10 @@ package dev.vality.disputes.schedule.handler;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.CreateAdjustmentsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
@Slf4j
public class CreateAdjustmentHandler {
private final CreateAdjustmentsService createAdjustmentsService;
@ -12,10 +14,13 @@ public class CreateAdjustmentHandler {
public Long handle(Dispute dispute) {
final var currentThread = Thread.currentThread();
final var oldName = currentThread.getName();
currentThread.setName("dispute-create-adjustment-" + dispute.getId());
currentThread.setName("dispute-created-adjustment-id-" + dispute.getId() + "-" + oldName);
try {
createAdjustmentsService.callHgForCreateAdjustment(dispute);
return dispute.getId();
} catch (Throwable ex) {
log.error("Received exception while scheduler processed callHgForCreateAdjustment", ex);
throw ex;
} finally {
currentThread.setName(oldName);
}

View File

@ -3,8 +3,10 @@ package dev.vality.disputes.schedule.handler;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.CreatedDisputesService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
@Slf4j
public class CreatedDisputeHandler {
private final CreatedDisputesService createdDisputesService;
@ -12,10 +14,13 @@ public class CreatedDisputeHandler {
public Long handle(Dispute dispute) {
final var currentThread = Thread.currentThread();
final var oldName = currentThread.getName();
currentThread.setName("dispute-created-" + dispute.getId());
currentThread.setName("dispute-created-id-" + dispute.getId() + "-" + oldName);
try {
createdDisputesService.callCreateDisputeRemotely(dispute);
return dispute.getId();
} catch (Throwable ex) {
log.error("Received exception while scheduler processed callCreateDisputeRemotely", ex);
throw ex;
} finally {
currentThread.setName(oldName);
}

View File

@ -3,8 +3,10 @@ package dev.vality.disputes.schedule.handler;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.PendingDisputesService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RequiredArgsConstructor
@Slf4j
public class PendingDisputeHandler {
private final PendingDisputesService pendingDisputesService;
@ -12,10 +14,13 @@ public class PendingDisputeHandler {
public Long handle(Dispute dispute) {
final var currentThread = Thread.currentThread();
final var oldName = currentThread.getName();
currentThread.setName("dispute-pending-" + dispute.getId());
currentThread.setName("dispute-pending-id-" + dispute.getId() + "-" + oldName);
try {
pendingDisputesService.callPendingDisputeRemotely(dispute);
return dispute.getId();
} catch (Throwable ex) {
log.error("Received exception while scheduler processed callPendingDisputeRemotely", ex);
throw ex;
} finally {
currentThread.setName(oldName);
}

View File

@ -49,9 +49,9 @@ public class CreateAdjustmentsService {
log.debug("GetDisputeForUpdateSkipLocked has been found {}", dispute);
var invoicePayment = getInvoicePayment(dispute);
if (invoicePayment == null || !invoicePayment.isSetRoute()) {
log.error("Trying to set failed Dispute status with PAYMENT_NOT_FOUND error reason {}", dispute);
log.error("Trying to set failed Dispute status with PAYMENT_NOT_FOUND error reason {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, ErrorReason.PAYMENT_NOT_FOUND);
log.debug("Dispute status has been set to failed {}", dispute);
log.debug("Dispute status has been set to failed {}", dispute.getId());
return;
}
var invoicePaymentAdjustment = adjustmentExtractor.searchAdjustmentByDispute(invoicePayment, dispute);
@ -59,16 +59,16 @@ public class CreateAdjustmentsService {
var changedAmount = adjustmentExtractor.getChangedAmount(invoicePaymentAdjustment.get(), dispute.getChangedAmount());
log.info("Trying to set succeeded Dispute status {}", dispute);
disputeDao.update(dispute.getId(), DisputeStatus.succeeded, changedAmount);
log.debug("Dispute status has been set to succeeded {}", dispute);
log.debug("Dispute status has been set to succeeded {}", dispute.getId());
return;
}
try {
var params = invoicePaymentAdjustmentParamsConverter.convert(dispute);
var paymentAdjustment = createAdjustment(dispute, params);
if (paymentAdjustment == null) {
log.error("Trying to set failed Dispute status with INVOICE_NOT_FOUND error reason {}", dispute);
log.error("Trying to set failed Dispute status with INVOICE_NOT_FOUND error reason {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, ErrorReason.INVOICE_NOT_FOUND);
log.debug("Dispute status has been set to failed {}", dispute);
log.debug("Dispute status has been set to failed {}", dispute.getId());
return;
}
} catch (InvoicingPaymentStatusPendingException e) {
@ -77,12 +77,12 @@ public class CreateAdjustmentsService {
// и тогда диспут будет пулиться, пока платеж не зафиналится,
// и тк никакой записи в коде выше нет, то пуллинг не проблема
// а запрос в checkDisputeStatus по идемпотентности просто вернет тот же success
log.error("Error when hg.createPaymentAdjustment() got payments status pending {}", dispute, e);
log.error("Error when hg.createPaymentAdjustment() got payments status pending {}", dispute.getId(), e);
return;
}
log.info("Trying to set succeeded Dispute status {}", dispute);
disputeDao.update(dispute.getId(), DisputeStatus.succeeded);
log.debug("Dispute status has been set to succeeded {}", dispute);
log.debug("Dispute status has been set to succeeded {}", dispute.getId());
}
private InvoicePaymentAdjustment createAdjustment(Dispute dispute, InvoicePaymentAdjustmentParams params) {

View File

@ -9,8 +9,6 @@ import dev.vality.disputes.service.external.FileStorageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@ -23,7 +21,6 @@ public class CreatedAttachmentsService {
private final FileMetaDao fileMetaDao;
private final FileStorageService fileStorageService;
@Transactional(propagation = Propagation.REQUIRED)
public List<Attachment> getAttachments(Dispute dispute) {
log.debug("Trying to get Attachments {}", dispute);
try {

View File

@ -19,7 +19,6 @@ import dev.vality.disputes.service.external.InvoicingService;
import dev.vality.geck.serializer.kit.tbase.TErrorUtil;
import dev.vality.woody.api.flow.error.WRuntimeException;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
@ -27,7 +26,6 @@ import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import static dev.vality.disputes.constant.TerminalOptionsField.DISPUTE_FLOW_CAPTURED_BLOCKED;
import static dev.vality.disputes.constant.TerminalOptionsField.DISPUTE_FLOW_PROVIDERS_API_EXIST;
@ -57,7 +55,6 @@ public class CreatedDisputesService {
}
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
@SneakyThrows
public void callCreateDisputeRemotely(Dispute dispute) {
log.debug("Trying to getDisputeForUpdateSkipLocked {}", dispute);
var forUpdate = disputeDao.getDisputeForUpdateSkipLocked(dispute.getId());
@ -68,39 +65,39 @@ public class CreatedDisputesService {
log.debug("GetDisputeForUpdateSkipLocked has been found {}", dispute);
var invoicePayment = getInvoicePayment(dispute);
if (invoicePayment == null || !invoicePayment.isSetRoute()) {
log.error("Trying to set failed Dispute status with PAYMENT_NOT_FOUND error reason {}", dispute);
log.error("Trying to set failed Dispute status with PAYMENT_NOT_FOUND error reason {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, ErrorReason.PAYMENT_NOT_FOUND);
log.debug("Dispute status has been set to failed {}", dispute);
log.debug("Dispute status has been set to failed {}", dispute.getId());
return;
}
var status = invoicePayment.getPayment().getStatus();
if (!status.isSetCaptured() && !status.isSetCancelled() && !status.isSetFailed()) {
// не создаем диспут, пока платеж не финален
log.warn("Payment has non-final status {} {}", status, dispute);
log.warn("Payment has non-final status {} {}", status, dispute.getId());
return;
}
var attachments = createdAttachmentsService.getAttachments(dispute);
if (attachments == null || attachments.isEmpty()) {
log.error("Trying to set failed Dispute status with NO_ATTACHMENTS error reason {}", dispute);
log.error("Trying to set failed Dispute status with NO_ATTACHMENTS error reason {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, ErrorReason.NO_ATTACHMENTS);
log.debug("Dispute status has been set to failed {}", dispute);
log.debug("Dispute status has been set to failed {}", dispute.getId());
return;
}
if ((status.isSetCaptured() && isCapturedBlockedForDispute(dispute))
|| isNotProvidersDisputesApiExist(dispute)) {
// отправлять на ручной разбор, если выставлена опция
// DISPUTE_FLOW_CAPTURED_BLOCKED или не выставлена DISPUTE_FLOW_PROVIDERS_API_EXIST
finishTaskWithManualParsingFlowActivation(dispute, attachments);
finishTaskWithManualParsingFlowActivation(dispute, attachments, DisputeStatus.manual_created);
return;
}
try {
var result = remoteClient.createDispute(dispute, attachments);
finishTask(dispute, result);
finishTask(dispute, attachments, result);
} catch (WRuntimeException e) {
if (externalGatewayChecker.isNotProvidersDisputesApiExist(dispute, e)) {
// отправлять на ручной разбор, если API диспутов на провайдере не реализовано
// (тогда при тесте соединения вернется 404)
finishTaskWithManualParsingFlowActivation(dispute, attachments);
finishTaskWithManualParsingFlowActivation(dispute, attachments, DisputeStatus.manual_created);
return;
}
throw e;
@ -108,41 +105,41 @@ public class CreatedDisputesService {
}
@Transactional(propagation = Propagation.REQUIRED)
void finishTask(Dispute dispute, DisputeCreatedResult result) {
void finishTask(Dispute dispute, List<Attachment> attachments, DisputeCreatedResult result) {
switch (result.getSetField()) {
case SUCCESS_RESULT -> {
var nextCheckAfter = exponentialBackOffPollingService.prepareNextPollingInterval(dispute);
log.info("Trying to set pending Dispute status {}, {}", dispute, result);
providerDisputeDao.save(new ProviderDispute(result.getSuccessResult().getProviderDisputeId(), dispute.getId()));
disputeDao.update(dispute.getId(), DisputeStatus.pending, nextCheckAfter);
log.debug("Dispute status has been set to pending {}", dispute);
log.debug("Dispute status has been set to pending {}", dispute.getId());
}
case FAIL_RESULT -> {
var errorMessage = TErrorUtil.toStringVal(result.getFailResult().getFailure());
log.warn("Trying to set failed Dispute status {}, {}", dispute, errorMessage);
log.warn("Trying to set failed Dispute status {}, {}", dispute.getId(), errorMessage);
disputeDao.update(dispute.getId(), DisputeStatus.failed, errorMessage);
log.debug("Dispute status has been set to failed {}", dispute);
log.debug("Dispute status has been set to failed {}", dispute.getId());
}
case ALREADY_EXIST_RESULT ->
finishTaskWithManualParsingFlowActivation(dispute, attachments, DisputeStatus.already_exist_created);
}
}
@Transactional(propagation = Propagation.REQUIRED)
void finishTaskWithManualParsingFlowActivation(Dispute dispute, List<Attachment> attachments) {
manualParsingTopic.sendCreated(dispute, attachments);
log.info("Trying to set manual_parsing_created Dispute status {}", dispute);
disputeDao.update(dispute.getId(), DisputeStatus.manual_created);
log.debug("Dispute status has been set to manual_parsing_created {}", dispute);
void finishTaskWithManualParsingFlowActivation(Dispute dispute, List<Attachment> attachments, DisputeStatus disputeStatus) {
manualParsingTopic.sendCreated(dispute, attachments, disputeStatus);
log.info("Trying to set {} Dispute status {}", disputeStatus, dispute);
disputeDao.update(dispute.getId(), disputeStatus);
log.debug("Dispute status has been set to {} {}", disputeStatus, dispute.getId());
}
@SneakyThrows
private boolean isCapturedBlockedForDispute(Dispute dispute) {
return getTerminal(dispute.getTerminalId()).get().getOptions()
return getTerminal(dispute.getTerminalId()).getOptions()
.containsKey(DISPUTE_FLOW_CAPTURED_BLOCKED);
}
@SneakyThrows
private boolean isNotProvidersDisputesApiExist(Dispute dispute) {
return !getTerminal(dispute.getTerminalId()).get().getOptions()
return !getTerminal(dispute.getTerminalId()).getOptions()
.containsKey(DISPUTE_FLOW_PROVIDERS_API_EXIST);
}
@ -150,7 +147,7 @@ public class CreatedDisputesService {
return invoicingService.getInvoicePayment(dispute.getInvoiceId(), dispute.getPaymentId());
}
private CompletableFuture<Terminal> getTerminal(Integer terminalId) {
private Terminal getTerminal(Integer terminalId) {
return dominantService.getTerminal(new TerminalRef(terminalId));
}
}

View File

@ -18,8 +18,6 @@ import org.apache.hc.core5.http.HttpStatus;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
@RequiredArgsConstructor
@ -43,20 +41,20 @@ public class ExternalGatewayChecker {
return httpClient.execute(new HttpGet(getRouteUrl(dispute)), isNotFoundResponse());
}
@SneakyThrows
private String getRouteUrl(Dispute dispute) {
return providerRouting.getRouteUrl(getTerminal(dispute.getTerminalId()).get().getOptions(), getProxy(dispute.getProviderId()).get().getUrl());
return providerRouting.getRouteUrl(getTerminal(dispute.getTerminalId()).getOptions(), getProxy(dispute.getProviderId()).getUrl());
}
private HttpClientResponseHandler<Boolean> isNotFoundResponse() {
return response -> response.getCode() == HttpStatus.SC_NOT_FOUND;
}
private CompletableFuture<Terminal> getTerminal(Integer terminalId) {
private Terminal getTerminal(Integer terminalId) {
return dominantService.getTerminal(new TerminalRef(terminalId));
}
private CompletableFuture<ProxyDefinition> getProxy(Integer providerId) {
return dominantService.getProxy(new ProviderRef(providerId));
private ProxyDefinition getProxy(Integer providerId) {
var provider = dominantService.getProvider(new ProviderRef(providerId));
return dominantService.getProxy(provider.getProxy().getRef());
}
}

View File

@ -48,23 +48,23 @@ public class PendingDisputesService {
return;
}
log.debug("GetDisputeForUpdateSkipLocked has been found {}", dispute);
log.debug("Trying to get ProviderDispute {}", dispute);
log.debug("Trying to get ProviderDispute {}", dispute.getId());
var providerDispute = providerDisputeDao.get(dispute.getId());
if (providerDispute == null) {
var nextCheckAfter = exponentialBackOffPollingService.prepareNextPollingInterval(dispute);
// вернуть в CreatedDisputeService и попробовать создать диспут в провайдере заново
log.error("Trying to set created Dispute status, because createDispute() was not success {}", dispute);
log.error("Trying to set created Dispute status, because createDispute() was not success {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.created, nextCheckAfter);
log.debug("Dispute status has been set to created {}", dispute);
log.debug("Dispute status has been set to created {}", dispute.getId());
return;
}
if (pollingInfoService.isDeadline(dispute)) {
log.error("Trying to set failed Dispute status with POOLING_EXPIRED error reason {}", dispute);
log.error("Trying to set failed Dispute status with POOLING_EXPIRED error reason {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, ErrorReason.POOLING_EXPIRED);
log.debug("Dispute status has been set to failed {}", dispute);
log.debug("Dispute status has been set to failed {}", dispute.getId());
return;
}
log.debug("ProviderDispute has been found {}", dispute);
log.debug("ProviderDispute has been found {}", dispute.getId());
var result = remoteClient.checkDisputeStatus(dispute, providerDispute);
finishTask(dispute, result);
}
@ -76,13 +76,13 @@ public class PendingDisputesService {
var changedAmount = result.getStatusSuccess().getChangedAmount().orElse(null);
log.info("Trying to set create_adjustment Dispute status {}, {}", dispute, result);
disputeDao.update(dispute.getId(), DisputeStatus.create_adjustment, changedAmount);
log.debug("Dispute status has been set to create_adjustment {}", dispute);
log.debug("Dispute status has been set to create_adjustment {}", dispute.getId());
}
case STATUS_FAIL -> {
var errorMessage = TErrorUtil.toStringVal(result.getStatusFail().getFailure());
log.warn("Trying to set failed Dispute status {}, {}", dispute, errorMessage);
log.warn("Trying to set failed Dispute status {}, {}", dispute.getId(), errorMessage);
disputeDao.update(dispute.getId(), DisputeStatus.failed, errorMessage);
log.debug("Dispute status has been set to failed {}", dispute);
log.debug("Dispute status has been set to failed {}", dispute.getId());
}
case STATUS_PENDING -> {
// дергаем update() чтоб обновить время вызова next_check_after,
@ -91,7 +91,7 @@ public class PendingDisputesService {
var nextCheckAfter = exponentialBackOffPollingService.prepareNextPollingInterval(dispute);
log.info("Trying to set pending Dispute status {}, {}", dispute, result);
disputeDao.update(dispute.getId(), DisputeStatus.pending, nextCheckAfter);
log.debug("Dispute status has been set to pending {}", dispute);
log.debug("Dispute status has been set to pending {}", dispute.getId());
}
}
}

View File

@ -5,6 +5,7 @@ import dev.vality.disputes.config.properties.AdaptersConnectionProperties;
import dev.vality.woody.thrift.impl.http.THSpawnClientBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.net.URI;
@ -14,20 +15,19 @@ import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings({"AbbreviationAsWordInName", "LineLength"})
public class ProviderIfaceBuilder {
private final ProviderRouting providerRouting;
private final AdaptersConnectionProperties adaptersConnectionProperties;
public ProviderDisputesServiceSrv.Iface build(Map<String, String> options, String url) {
return build(providerRouting.getRouteUrl(options, url));
}
private ProviderDisputesServiceSrv.Iface build(String url) {
log.info("Creating new client for url: {}", url);
@Cacheable(value = "adapters", key = "#root.args[1]", cacheManager = "adaptersCacheManager")
public ProviderDisputesServiceSrv.Iface buildTHSpawnClient(Map<String, String> options, String url) {
var routeUrl = providerRouting.getRouteUrl(options, url);
log.info("Creating new client for url: {}", routeUrl);
return new THSpawnClientBuilder()
.withNetworkTimeout((int) TimeUnit.SECONDS.toMillis(adaptersConnectionProperties.getTimeoutSec()))
.withAddress(URI.create(url))
.withAddress(URI.create(routeUrl))
.build(ProviderDisputesServiceSrv.Iface.class);
}
}

View File

@ -3,12 +3,11 @@ package dev.vality.disputes.schedule.service;
import dev.vality.disputes.exception.RoutingException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URL;
import java.net.URI;
import java.util.Map;
@Slf4j
@ -19,7 +18,6 @@ public class ProviderRouting {
private static final String DISPUTES_URL_POSTFIX_DEFAULT = "disputes";
private static final String OPTION_DISPUTES_URL_FIELD_NAME = "disputes_url";
@Cacheable(value = "adapters", key = "defaultProviderUrl", cacheManager = "adaptersCacheManager")
public String getRouteUrl(Map<String, String> options, String defaultProviderUrl) {
var url = options.get(OPTION_DISPUTES_URL_FIELD_NAME);
if (ObjectUtils.isEmpty(url)) {
@ -31,8 +29,7 @@ public class ProviderRouting {
private String createDefaultRouteUrl(String defaultProviderUrl) {
log.debug("Creating url by appending postfix");
try {
var validUri = new URL(defaultProviderUrl).toURI();
return UriComponentsBuilder.fromUri(validUri)
return UriComponentsBuilder.fromUri(URI.create(defaultProviderUrl))
.pathSegment(DISPUTES_URL_POSTFIX_DEFAULT)
.encode()
.build()

View File

@ -90,7 +90,7 @@ public class AccessService {
}
private InvoicePayment getInvoicePayment(dev.vality.damsel.payment_processing.Invoice invoice, String paymentId) {
log.debug("Processing invoice: {}", invoice);
log.debug("Processing invoice: {}", invoice.getInvoice().getId());
return invoice.getPayments().stream()
.filter(invoicePayment -> paymentId.equals(invoicePayment.getPayment().getId())
&& invoicePayment.isSetRoute())

View File

@ -15,6 +15,7 @@ import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength"})
public class BouncerServiceImpl implements BouncerService {
private final BouncerProperties bouncerProperties;
@ -23,9 +24,9 @@ public class BouncerServiceImpl implements BouncerService {
@Override
public Resolution getResolution(AccessData accessData) {
log.debug("Check access with bouncer context: {}", accessData);
log.debug("Check access with bouncer context: {}{}", accessData.getInvoice().getInvoice().getId(), accessData.getPayment().getPayment().getId());
var context = bouncerContextFactory.buildContext(accessData);
log.debug("Built thrift context: {}", context);
log.debug("Built thrift context: {}{}", accessData.getInvoice().getInvoice().getId(), accessData.getPayment().getPayment().getId());
try {
var judge = bouncerClient.judge(bouncerProperties.getRuleSetId(), context);
log.debug("Have judge: {}", judge);

View File

@ -2,14 +2,14 @@ package dev.vality.disputes.service.external;
import dev.vality.damsel.domain.*;
import java.util.concurrent.CompletableFuture;
public interface DominantService {
Currency getCurrency(CurrencyRef currencyRef);
CompletableFuture<Terminal> getTerminal(TerminalRef terminalRef);
Terminal getTerminal(TerminalRef terminalRef);
CompletableFuture<ProxyDefinition> getProxy(ProviderRef providerRef);
ProxyDefinition getProxy(ProxyRef proxyRef);
Provider getProvider(ProviderRef providerRef);
}

View File

@ -6,11 +6,8 @@ import dev.vality.disputes.service.external.DominantService;
import dev.vality.disputes.service.external.impl.dominant.DominantCacheServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
@RequiredArgsConstructor
@ -23,26 +20,18 @@ public class DominantServiceImpl implements DominantService {
return dominantCacheService.getCurrency(currencyRef);
}
@Async
@Override
public CompletableFuture<Terminal> getTerminal(TerminalRef terminalRef) {
try {
var terminal = dominantCacheService.getTerminal(terminalRef);
return CompletableFuture.completedFuture(terminal);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
public Terminal getTerminal(TerminalRef terminalRef) {
return dominantCacheService.getTerminal(terminalRef);
}
@Async
@Override
public CompletableFuture<ProxyDefinition> getProxy(ProviderRef providerRef) {
try {
var provider = dominantCacheService.getProvider(providerRef);
var proxy = dominantCacheService.getProxy(provider.getProxy().getRef());
return CompletableFuture.completedFuture(proxy);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
public ProxyDefinition getProxy(ProxyRef proxyRef) {
return dominantCacheService.getProxy(proxyRef);
}
@Override
public Provider getProvider(ProviderRef providerRef) {
return dominantCacheService.getProvider(providerRef);
}
}

View File

@ -0,0 +1,41 @@
package dev.vality.disputes.service.external.impl.dominant;
import dev.vality.damsel.domain.Currency;
import dev.vality.damsel.domain.CurrencyRef;
import dev.vality.damsel.domain.Terminal;
import dev.vality.damsel.domain.TerminalRef;
import dev.vality.disputes.service.external.DominantService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
@RequiredArgsConstructor
public class DominantAsyncService {
private final DominantService dominantService;
@Async("dominantAsyncServiceExecutor")
public CompletableFuture<Currency> getCurrency(CurrencyRef currencyRef) {
try {
var currency = dominantService.getCurrency(currencyRef);
return CompletableFuture.completedFuture(currency);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
@Async("dominantAsyncServiceExecutor")
public CompletableFuture<Terminal> getTerminal(TerminalRef terminalRef) {
try {
var terminal = dominantService.getTerminal(terminalRef);
return CompletableFuture.completedFuture(terminal);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
}
}

View File

@ -19,20 +19,19 @@ public class DominantCacheServiceImpl {
private final RepositoryClientSrv.Iface dominantClient;
@Cacheable(value = "currencies", key = "#currencyRef.symbolic_code", cacheManager = "currenciesCacheManager")
public Currency getCurrency(CurrencyRef currencyRef) throws NotFoundException {
public Currency getCurrency(CurrencyRef currencyRef) {
return getCurrency(currencyRef, Reference.head(new Head()));
}
private Currency getCurrency(CurrencyRef currencyRef, Reference revisionReference)
throws NotFoundException {
private Currency getCurrency(CurrencyRef currencyRef, Reference revisionReference) {
log.debug("Trying to get currency, currencyRef='{}', revisionReference='{}'", currencyRef, revisionReference);
try {
var reference = new dev.vality.damsel.domain.Reference();
reference.setCurrency(currencyRef);
var versionedObject = checkoutObject(revisionReference, reference);
var currency = versionedObject.getObject().getCurrency().getData();
log.debug("Currency has been found, currencyRef='{}', revisionReference='{}', currency='{}'",
currencyRef, revisionReference, currency);
log.debug("Currency has been found, currencyRef='{}', revisionReference='{}'",
currencyRef, revisionReference);
return currency;
} catch (VersionNotFound | ObjectNotFound ex) {
throw new NotFoundException(String.format("Version not found, currencyRef='%s', revisionReference='%s'",
@ -48,8 +47,7 @@ public class DominantCacheServiceImpl {
return getTerminal(terminalRef, Reference.head(new Head()));
}
public Terminal getTerminal(TerminalRef terminalRef, Reference revisionReference)
throws NotFoundException {
public Terminal getTerminal(TerminalRef terminalRef, Reference revisionReference) {
log.debug("Trying to get terminal from dominant, terminalRef='{}', revisionReference='{}'", terminalRef,
revisionReference);
try {
@ -57,8 +55,8 @@ public class DominantCacheServiceImpl {
reference.setTerminal(terminalRef);
var versionedObject = checkoutObject(revisionReference, reference);
var terminal = versionedObject.getObject().getTerminal().getData();
log.debug("Terminal has been found, terminalRef='{}', revisionReference='{}', terminal='{}'",
terminalRef, revisionReference, terminal);
log.debug("Terminal has been found, terminalRef='{}', revisionReference='{}'",
terminalRef, revisionReference);
return terminal;
} catch (VersionNotFound | ObjectNotFound ex) {
throw new NotFoundException(String.format("Version not found, terminalRef='%s', revisionReference='%s'",
@ -74,8 +72,7 @@ public class DominantCacheServiceImpl {
return getProvider(providerRef, Reference.head(new Head()));
}
private Provider getProvider(ProviderRef providerRef, Reference revisionReference)
throws NotFoundException {
private Provider getProvider(ProviderRef providerRef, Reference revisionReference) {
log.debug("Trying to get provider from dominant, providerRef='{}', revisionReference='{}'", providerRef,
revisionReference);
try {
@ -83,8 +80,8 @@ public class DominantCacheServiceImpl {
reference.setProvider(providerRef);
var versionedObject = checkoutObject(revisionReference, reference);
var provider = versionedObject.getObject().getProvider().getData();
log.debug("Provider has been found, providerRef='{}', revisionReference='{}', terminal='{}'",
providerRef, revisionReference, provider);
log.debug("Provider has been found, providerRef='{}', revisionReference='{}'",
providerRef, revisionReference);
return provider;
} catch (VersionNotFound | ObjectNotFound ex) {
throw new NotFoundException(String.format("Version not found, providerRef='%s', revisionReference='%s'",
@ -101,8 +98,7 @@ public class DominantCacheServiceImpl {
}
private ProxyDefinition getProxy(ProxyRef proxyRef, Reference revisionReference)
throws NotFoundException {
private ProxyDefinition getProxy(ProxyRef proxyRef, Reference revisionReference) {
log.debug("Trying to get proxy from dominant, proxyRef='{}', revisionReference='{}'", proxyRef,
revisionReference);
try {

View File

@ -91,6 +91,9 @@ dispute:
fixedDelayCreated: 5000
fixedDelayPending: 5000
fixedDelayCreateAdjustments: 5000
initialDelayCreated: 5000
initialDelayPending: 5000
initialDelayCreateAdjustments: 5000
isScheduleCreatedEnabled: true
isSchedulePendingEnabled: true
isScheduleCreateAdjustmentsEnabled: true
@ -105,3 +108,10 @@ manual-parsing-topic:
testcontainers:
postgresql:
tag: '11.4'
http-client:
requestTimeout: 60000
poolTimeout: 10000
connectionTimeout: 10000
maxTotalPooling: 200
defaultMaxPerRoute: 200

View File

@ -0,0 +1 @@
ALTER TYPE dspt.dispute_status ADD VALUE 'already_exist_created';

View File

@ -1,26 +1,47 @@
package dev.vality.disputes.dao;
import dev.vality.disputes.dao.config.PostgresqlSpringBootITest;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.testcontainers.annotations.DefaultSpringBootTest;
import dev.vality.testcontainers.annotations.postgresql.PostgresqlTestcontainerSingleton;
import dev.vality.disputes.exception.NotFoundException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import static dev.vality.testcontainers.annotations.util.RandomBeans.random;
import static dev.vality.testcontainers.annotations.util.ValuesGenerator.generateId;
import static dev.vality.testcontainers.annotations.util.ValuesGenerator.generateLong;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@PostgresqlTestcontainerSingleton
@DefaultSpringBootTest
@PostgresqlSpringBootITest
public class DisputeDaoTest {
@Autowired
private DisputeDao disputeDao;
@Test
public void insertAndFindTest() {
public void testInsertAndFind() {
var random = random(Dispute.class);
disputeDao.save(random);
assertEquals(random,
disputeDao.get(random.getId(), random.getInvoiceId(), random.getPaymentId()));
}
@Test
public void testNotFoundException() {
assertThrows(NotFoundException.class,
() -> disputeDao.get(generateLong(), generateId(), generateId()));
}
@Test
public void testMultiInsertAndFind() {
var random = random(Dispute.class);
random.setId(null);
random.setInvoiceId("setInvoiceId");
random.setPaymentId("setPaymentId");
disputeDao.save(random);
disputeDao.save(random);
disputeDao.save(random);
assertEquals(3,
disputeDao.get(random.getInvoiceId(), random.getPaymentId()).size());
}
}

View File

@ -0,0 +1,19 @@
package dev.vality.disputes.dao.config;
import dev.vality.disputes.dao.config.testconfiguration.MockedUnimportantServicesConfig;
import dev.vality.testcontainers.annotations.DefaultSpringBootTest;
import dev.vality.testcontainers.annotations.postgresql.PostgresqlTestcontainerSingleton;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PostgresqlTestcontainerSingleton
@DefaultSpringBootTest
@Import(MockedUnimportantServicesConfig.class)
public @interface PostgresqlSpringBootITest {
}

View File

@ -0,0 +1,21 @@
package dev.vality.disputes.dao.config.testconfiguration;
import dev.vality.disputes.schedule.TaskCreateAdjustmentsService;
import dev.vality.disputes.schedule.TaskCreatedDisputesService;
import dev.vality.disputes.schedule.TaskPendingDisputesService;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
@TestConfiguration
public class MockedUnimportantServicesConfig {
@MockBean
private TaskCreatedDisputesService taskCreatedDisputesService;
@MockBean
private TaskPendingDisputesService taskPendingDisputesService;
@MockBean
private TaskCreateAdjustmentsService taskCreateAdjustmentsService;
}