add disputes-tg-bot usage (#23)

* rename AdminManagementServlet

* add disputes-tg-bot usage

* add dummy for tg bot usages

* add mapping

* remove texeption catcher

* bumps deps, remove debug endpoint

* rename

* rename

* disable isScheduleReadyForCreateAdjustmentsEnabled

* fix tests

* up pg versino

* refactor ProviderRouting

* add handleUnexpectedResultMapping tests cases

* checkstyle

* maven-site issue

* bump workflow

* bump workflow

* bump workflow

* review fixes
This commit is contained in:
Anatolii Karlov 2024-10-29 17:49:03 +07:00 committed by GitHub
parent 0f39485afc
commit df25417b6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
48 changed files with 982 additions and 213 deletions

View File

@ -48,7 +48,7 @@
Если при финальном статусе платежа `captured` создавать на провайдере диспут является не желательной ситуацией, можно
установить опцию в терминале `DISPUTE_FLOW_CAPTURED_BLOCKED` и пулять
состояние
в топик\тг-провайдер-бот\filebeat на ручной разбор (`ManualParsing` module)
в топик\тг-провайдер-бот\filebeat на ручной разбор (`AdminManagement` module)
Не все провайдеры на данный момент поддерживают работу с диспутами по `API`.
Предполагается такой способ действия при этой ситуации:
@ -111,7 +111,7 @@
- если это captured платеж и выставлена опция `DISPUTE_FLOW_CAPTURED_BLOCKED` , то тоже отправляет на ручной разбор
Далее, через внутрений трифт-интерфейс саппорт получает способ манипулировать диспутом для его
обработки (`ManualParsingDisputesService`)
обработки (`AdminManagementDisputesService`)
- Перед переводом диспута в финальный статус саппорт должен будет забиндить айди созданного диспута в провайдере через
ручку `BindCreated()`. Здесь особенность, что этот метод фильтрует возможность биндить диспуты только созданные

20
pom.xml
View File

@ -6,7 +6,7 @@
<parent>
<groupId>dev.vality</groupId>
<artifactId>service-parent-pom</artifactId>
<version>3.0.2</version>
<version>3.0.4</version>
</parent>
<artifactId>disputes-api</artifactId>
@ -47,7 +47,7 @@
<dependency>
<groupId>dev.vality</groupId>
<artifactId>disputes-proto</artifactId>
<version>1.23-37a5ad1</version>
<version>1.26-fc8e34f</version>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
@ -62,6 +62,7 @@
<dependency>
<groupId>dev.vality</groupId>
<artifactId>damsel</artifactId>
<version>1.648-ad715bd</version>
</dependency>
<dependency>
<groupId>dev.vality</groupId>
@ -87,6 +88,16 @@
<artifactId>adapter-flow-lib</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>dev.vality.woody</groupId>
<artifactId>woody-thrift</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>dev.vality.woody</groupId>
<artifactId>woody-api</artifactId>
<version>2.0.8</version>
</dependency>
<!--spring-->
<dependency>
@ -217,6 +228,11 @@
<artifactId>guava</artifactId>
<version>32.0.0-jre</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-semconv</artifactId>
<version>1.29.0-alpha</version>
</dependency>
<!--test-->
<dependency>

View File

@ -0,0 +1,17 @@
package dev.vality.disputes.admin.callback;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import java.util.List;
public interface CallbackNotifier {
void sendDisputeAlreadyCreated(Dispute dispute);
void sendDisputePoolingExpired(Dispute dispute);
void sendDisputeReadyForCreateAdjustment(List<Dispute> disputes);
void sendDisputeFailedReviewRequired(Dispute dispute, String errorCode, String errorDescription);
}

View File

@ -0,0 +1,52 @@
package dev.vality.disputes.admin.callback;
import dev.vality.disputes.admin.DisputeAlreadyCreated;
import dev.vality.disputes.admin.DisputeFailedReviewRequired;
import dev.vality.disputes.admin.DisputePoolingExpired;
import dev.vality.disputes.admin.DisputeReadyForCreateAdjustment;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.service.external.DisputesTgBotService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
@Service
@ConditionalOnProperty(value = "service.disputes-tg-bot.admin.enabled", havingValue = "true", matchIfMissing = true)
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings({"LineLength"})
public class DisputesTgBotCallbackNotifierImpl implements CallbackNotifier {
private final DisputesTgBotService disputesTgBotService;
@Override
public void sendDisputeAlreadyCreated(Dispute dispute) {
disputesTgBotService.sendDisputeAlreadyCreated(new DisputeAlreadyCreated(dispute.getId().toString()));
}
@Override
public void sendDisputePoolingExpired(Dispute dispute) {
disputesTgBotService.sendDisputePoolingExpired(new DisputePoolingExpired(dispute.getId().toString()));
}
@Override
public void sendDisputeReadyForCreateAdjustment(List<Dispute> disputes) {
var disputeReadyForCreateAdjustments = disputes.stream()
.map(Dispute::getId)
.map(UUID::toString)
.map(DisputeReadyForCreateAdjustment::new)
.toList();
disputesTgBotService.sendDisputeReadyForCreateAdjustment(disputeReadyForCreateAdjustments);
}
@Override
public void sendDisputeFailedReviewRequired(Dispute dispute, String errorCode, String errorDescription) {
disputesTgBotService.sendDisputeFailedReviewRequired(
new DisputeFailedReviewRequired(dispute.getId().toString(), errorCode)
.setErrorDescription(errorDescription));
}
}

View File

@ -0,0 +1,37 @@
package dev.vality.disputes.admin.callback;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@ConditionalOnProperty(value = "service.disputes-tg-bot.admin.enabled", havingValue = "false")
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings({"LineLength"})
public class DummyCallbackNotifierImpl implements CallbackNotifier {
@Override
public void sendDisputeAlreadyCreated(Dispute dispute) {
log.debug("Trying to call DummyCallbackNotifierImpl.sendDisputeAlreadyCreated() {}", dispute.getId());
}
@Override
public void sendDisputePoolingExpired(Dispute dispute) {
log.debug("Trying to call DummyCallbackNotifierImpl.sendDisputePoolingExpired() {}", dispute.getId());
}
@Override
public void sendDisputeReadyForCreateAdjustment(List<Dispute> disputes) {
log.debug("Trying to call DummyCallbackNotifierImpl.sendDisputeReadyForCreateAdjustment() {}", disputes.size());
}
@Override
public void sendDisputeFailedReviewRequired(Dispute dispute, String errorCode, String errorDescription) {
log.debug("Trying to call DummyCallbackNotifierImpl.sendDisputeFailedReviewRequired() {}", dispute.getId());
}
}

View File

@ -1,4 +1,4 @@
package dev.vality.disputes.manualparsing;
package dev.vality.disputes.admin.management;
import dev.vality.disputes.admin.*;
import dev.vality.disputes.dao.DisputeDao;
@ -31,7 +31,7 @@ import static dev.vality.disputes.api.service.ApiDisputesService.DISPUTE_PENDING
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
public class ManualParsingDisputesService {
public class AdminManagementDisputesService {
private final DisputeDao disputeDao;
private final ProviderDisputeDao providerDisputeDao;
@ -48,11 +48,12 @@ public class ManualParsingDisputesService {
return;
}
var cancelReason = cancelParams.getCancelReason().orElse(null);
var mapping = cancelParams.getMapping().orElse(null);
log.debug("GetForUpdateSkipLocked has been found {}", dispute);
if (DISPUTE_PENDING.contains(dispute.getStatus())) {
// используется не failed, а cancelled чтоб можно было понять, что зафейлен по внешнему вызову
log.warn("Trying to set cancelled Dispute status {}, {}", dispute, cancelReason);
disputeDao.update(dispute.getId(), DisputeStatus.cancelled, cancelReason);
log.warn("Trying to set cancelled Dispute status {}, {}, {}", dispute, mapping, cancelReason);
disputeDao.update(dispute.getId(), DisputeStatus.cancelled, cancelReason, mapping);
log.debug("Dispute status has been set to cancelled {}", dispute);
} else {
log.info("Request was skipped by inappropriate status {}", dispute);
@ -133,6 +134,7 @@ public class ManualParsingDisputesService {
disputeResult.setProviderTrxId(dispute.getProviderTrxId());
disputeResult.setStatus(dispute.getStatus().name());
disputeResult.setErrorMessage(dispute.getErrorMessage());
disputeResult.setMapping(dispute.getMapping());
disputeResult.setAmount(String.valueOf(dispute.getAmount()));
disputeResult.setChangedAmount(Optional.ofNullable(dispute.getChangedAmount())
.map(String::valueOf)

View File

@ -1,4 +1,4 @@
package dev.vality.disputes.manualparsing;
package dev.vality.disputes.admin.management;
import dev.vality.disputes.admin.*;
import lombok.RequiredArgsConstructor;
@ -12,28 +12,28 @@ import java.util.ArrayList;
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings({"ParameterName", "LineLength"})
public class ManualParsingHandler implements ManualParsingServiceSrv.Iface {
public class AdminManagementHandler implements AdminManagementServiceSrv.Iface {
private final ManualParsingDisputesService manualParsingDisputesService;
private final AdminManagementDisputesService adminManagementDisputesService;
@Override
public void cancelPending(CancelParamsRequest cancelParamsRequest) throws TException {
for (var cancelParam : cancelParamsRequest.getCancelParams()) {
manualParsingDisputesService.cancelPendingDispute(cancelParam);
adminManagementDisputesService.cancelPendingDispute(cancelParam);
}
}
@Override
public void approvePending(ApproveParamsRequest approveParamsRequest) throws TException {
for (var approveParam : approveParamsRequest.getApproveParams()) {
manualParsingDisputesService.approvePendingDispute(approveParam);
adminManagementDisputesService.approvePendingDispute(approveParam);
}
}
@Override
public void bindCreated(BindParamsRequest bindParamsRequest) throws TException {
for (var bindParam : bindParamsRequest.getBindParams()) {
manualParsingDisputesService.bindCreatedDispute(bindParam);
adminManagementDisputesService.bindCreatedDispute(bindParam);
}
}
@ -41,7 +41,7 @@ public class ManualParsingHandler implements ManualParsingServiceSrv.Iface {
public DisputeResult getDisputes(DisputeParamsRequest disputeParamsRequest) throws TException {
var disputeResult = new DisputeResult(new ArrayList<>());
for (var disputeParams : disputeParamsRequest.getDisputeParams()) {
var dispute = manualParsingDisputesService.getDispute(disputeParams, disputeParamsRequest.isWithAttachments());
var dispute = adminManagementDisputesService.getDispute(disputeParams, disputeParamsRequest.isWithAttachments());
if (dispute != null) {
disputeResult.getDisputes().add(dispute);
}

View File

@ -1,8 +1,7 @@
package dev.vality.disputes.manualparsing;
package dev.vality.disputes.admin.management;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.provider.Attachment;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
@ -11,26 +10,28 @@ import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings({"ParameterName", "LineLength"})
public class ManualParsingTopic {
public class MdcTopicProducer {
@Value("${manual-parsing-topic.enabled}")
@Value("${service.mdc-topic-producer.enabled}")
private boolean enabled;
public void sendCreated(Dispute dispute, List<Attachment> attachments, DisputeStatus disputeStatus) {
public void sendCreated(Dispute dispute, DisputeStatus disputeStatus, String errorMessage) {
if (!enabled) {
return;
}
var contextMap = MDC.getCopyOfContextMap() == null ? new HashMap<String, String>() : MDC.getCopyOfContextMap();
var contextMap = getContextMap();
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.name());
if (errorMessage != null) {
contextMap.put("dispute_error_message", errorMessage);
}
MDC.setContextMap(contextMap);
log.warn("Manual parsing case");
MDC.clear();
@ -40,7 +41,7 @@ public class ManualParsingTopic {
if (!enabled) {
return;
}
var contextMap = MDC.getCopyOfContextMap() == null ? new HashMap<String, String>() : MDC.getCopyOfContextMap();
var contextMap = getContextMap();
contextMap.put("dispute_id", dispute.getId().toString());
contextMap.put("dispute_status", DisputeStatus.manual_pending.name());
MDC.setContextMap(contextMap);
@ -52,11 +53,15 @@ public class ManualParsingTopic {
if (!enabled || disputes.isEmpty()) {
return;
}
var contextMap = MDC.getCopyOfContextMap() == null ? new HashMap<String, String>() : MDC.getCopyOfContextMap();
var contextMap = getContextMap();
contextMap.put("dispute_ids", disputes.stream().map(Dispute::getId).map(String::valueOf).collect(Collectors.joining(", ")));
contextMap.put("dispute_status", DisputeStatus.create_adjustment.name());
MDC.setContextMap(contextMap);
log.warn("Ready for CreateAdjustments case");
MDC.clear();
}
private Map<String, String> getContextMap() {
return MDC.getCopyOfContextMap() == null ? new HashMap<>() : MDC.getCopyOfContextMap();
}
}

View File

@ -1,30 +0,0 @@
package dev.vality.disputes.api;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import dev.vality.disputes.admin.CancelParamsRequest;
import dev.vality.disputes.admin.ManualParsingServiceSrv;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping({"/disputes"})
@Slf4j
public class CancelController {
private final ManualParsingServiceSrv.Iface manualParsingHandler;
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module());
@PostMapping("/cancel")
@SneakyThrows
public void cancelPending(@RequestBody String body) {
log.debug("cancelPending {}", body);
manualParsingHandler.cancelPending(objectMapper.readValue(body, CancelParamsRequest.class));
}
}

View File

@ -12,8 +12,8 @@ public class Status200ResponseConverter {
public Status200Response convert(Dispute dispute) {
var body = new Status200Response();
body.setStatus(getStatus(dispute));
if (!StringUtils.isBlank(dispute.getErrorMessage())) {
body.setReason(new GeneralError(dispute.getErrorMessage()));
if (!StringUtils.isBlank(dispute.getMapping())) {
body.setReason(new GeneralError(dispute.getMapping()));
}
if (dispute.getChangedAmount() != null) {
body.setChangedAmount(dispute.getChangedAmount());

View File

@ -5,6 +5,8 @@ import dev.vality.bouncer.decisions.ArbiterSrv;
import dev.vality.damsel.domain_config.RepositoryClientSrv;
import dev.vality.damsel.payment_processing.InvoicingSrv;
import dev.vality.damsel.payment_processing.PartyManagementSrv;
import dev.vality.disputes.admin.AdminCallbackServiceSrv;
import dev.vality.disputes.provider.ProviderDisputesServiceSrv;
import dev.vality.file.storage.FileStorageSrv;
import dev.vality.token.keeper.TokenAuthenticatorSrv;
import dev.vality.woody.thrift.impl.http.THSpawnClientBuilder;
@ -81,6 +83,26 @@ public class ApplicationConfig {
.build(PartyManagementSrv.Iface.class);
}
@Bean
public ProviderDisputesServiceSrv.Iface providerDisputesTgBotClient(
@Value("${service.disputes-tg-bot.provider.url}") Resource resource,
@Value("${service.disputes-tg-bot.provider.networkTimeout}") int networkTimeout) throws IOException {
return new THSpawnClientBuilder()
.withNetworkTimeout(networkTimeout)
.withAddress(resource.getURI())
.build(ProviderDisputesServiceSrv.Iface.class);
}
@Bean
public AdminCallbackServiceSrv.Iface adminCallbackDisputesTgBotClient(
@Value("${service.disputes-tg-bot.admin.url}") Resource resource,
@Value("${service.disputes-tg-bot.admin.networkTimeout}") int networkTimeout) throws IOException {
return new THSpawnClientBuilder()
.withNetworkTimeout(networkTimeout)
.withAddress(resource.getURI())
.build(AdminCallbackServiceSrv.Iface.class);
}
@Bean
public ExecutorService disputesThreadPool(@Value("${dispute.batchSize}") int threadPoolSize) {
final var threadFactory = new ThreadFactoryBuilder()

View File

@ -24,7 +24,7 @@ public class NetworkConfig {
public static final String HEALTH = "/actuator/health";
public static final String MERCHANT = "/disputes-api/v1/merchant";
public static final String MANUAL = "/disputes-api/v1/manual-parsing";
public static final String ADMIN_MANAGEMENT = "/disputes-api/v1/admin-management";
public static final String CALLBACK = "/disputes-api/v1/callback";
@Bean
@ -39,7 +39,7 @@ public class NetworkConfig {
var enabledPaths = servletPath.startsWith(restEndpoint)
|| servletPath.startsWith(HEALTH)
|| servletPath.startsWith(MERCHANT)
|| servletPath.startsWith(MANUAL)
|| servletPath.startsWith(ADMIN_MANAGEMENT)
|| servletPath.startsWith(CALLBACK);
if ((request.getLocalPort() == restPort) && !enabledPaths) {
response.sendError(404, "Unknown address");

View File

@ -0,0 +1,68 @@
package dev.vality.disputes.config;
import dev.vality.disputes.config.properties.OtelProperties;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.sdk.trace.samplers.Sampler;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class OtelConfig {
private final OtelProperties otelProperties;
@Value("${spring.application.name}")
private String applicationName;
@Bean
public OpenTelemetry openTelemetryConfig() {
var resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, applicationName)));
var sdkTracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(BatchSpanProcessor.builder(OtlpHttpSpanExporter.builder()
.setEndpoint(otelProperties.getResource())
.setTimeout(Duration.ofMillis(otelProperties.getTimeout()))
.build())
.build())
.setSampler(Sampler.alwaysOn())
.setResource(resource)
.build();
var openTelemetrySdk = OpenTelemetrySdk.builder()
.setTracerProvider(sdkTracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.build();
registerGlobalOpenTelemetry(openTelemetrySdk);
return openTelemetrySdk;
}
private static void registerGlobalOpenTelemetry(OpenTelemetry openTelemetry) {
try {
GlobalOpenTelemetry.set(openTelemetry);
} catch (Exception e) {
log.warn("please initialize the ObservabilitySdk before starting the application");
GlobalOpenTelemetry.resetForTest();
try {
GlobalOpenTelemetry.set(openTelemetry);
} catch (Exception ex) {
log.warn("unable to set GlobalOpenTelemetry", ex);
}
}
}
}

View File

@ -0,0 +1,17 @@
package dev.vality.disputes.config.properties;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "otel")
public class OtelProperties {
private String resource;
private Long timeout;
}

View File

@ -0,0 +1,7 @@
package dev.vality.disputes.constant;
public class ModerationPrefix {
public static final String DISPUTES_UNKNOWN_MAPPING = "disputes_unknown_mapping";
}

View File

@ -113,28 +113,32 @@ public class DisputeDao extends AbstractGenericDao {
}
public UUID update(UUID disputeId, DisputeStatus status) {
return update(disputeId, status, null, null, null, null);
return update(disputeId, status, null, null, null, null, null);
}
public UUID update(UUID disputeId, DisputeStatus status, LocalDateTime nextCheckAfter) {
return update(disputeId, status, nextCheckAfter, null, null, null);
return update(disputeId, status, nextCheckAfter, null, null, null, null);
}
public UUID update(UUID disputeId, DisputeStatus status, String errorMessage) {
return update(disputeId, status, null, errorMessage, null, null);
return update(disputeId, status, null, errorMessage, null, null, null);
}
public UUID update(UUID disputeId, DisputeStatus status, String errorMessage, String mapping) {
return update(disputeId, status, null, errorMessage, null, null, mapping);
}
public UUID update(UUID disputeId, DisputeStatus status, Long changedAmount) {
return update(disputeId, status, null, null, changedAmount, null);
return update(disputeId, status, null, null, changedAmount, null, null);
}
public UUID update(UUID disputeId, DisputeStatus status, Long changedAmount,
Boolean skipCallHgForCreateAdjustment) {
return update(disputeId, status, null, null, changedAmount, skipCallHgForCreateAdjustment);
return update(disputeId, status, null, null, changedAmount, skipCallHgForCreateAdjustment, null);
}
private UUID update(UUID disputeId, DisputeStatus status, LocalDateTime nextCheckAfter, String errorMessage,
Long changedAmount, Boolean skipCallHgForCreateAdjustment) {
Long changedAmount, Boolean skipCallHgForCreateAdjustment, String mapping) {
var set = getDslContext().update(DISPUTE)
.set(DISPUTE.STATUS, status);
if (nextCheckAfter != null) {
@ -143,6 +147,9 @@ public class DisputeDao extends AbstractGenericDao {
if (errorMessage != null) {
set = set.set(DISPUTE.ERROR_MESSAGE, errorMessage);
}
if (mapping != null) {
set = set.set(DISPUTE.MAPPING, mapping);
}
if (changedAmount != null) {
set = set.set(DISPUTE.CHANGED_AMOUNT, changedAmount);
}

View File

@ -31,11 +31,12 @@ public class MerchantDisputesHandler implements MerchantDisputesServiceSrv.Iface
}
@Override
public DisputeStatusResult checkDisputeStatus(DisputeContext disputeContext) throws DisputeNotFound, TException {
public DisputeStatusResult checkDisputeStatus(DisputeContext disputeContext) throws TException {
var response = disputesApiDelegate.status(getRequestID(), disputeContext.getDisputeId(), false).getBody();
return switch (response.getStatus()) {
case PENDING -> DisputeStatusResult.statusPending(new DisputeStatusPendingResult());
case FAILED -> DisputeStatusResult.statusFail(new DisputeStatusFailResult(getErrorMessage(response)));
case FAILED ->
DisputeStatusResult.statusFail(new DisputeStatusFailResult().setMapping(getMapping(response)));
case SUCCEEDED -> DisputeStatusResult.statusSuccess(new DisputeStatusSuccessResult());
};
}
@ -44,7 +45,7 @@ public class MerchantDisputesHandler implements MerchantDisputesServiceSrv.Iface
return UUID.randomUUID().toString();
}
private String getErrorMessage(Status200Response response) {
private String getMapping(Status200Response response) {
return Optional.ofNullable(response.getReason())
.map(GeneralError::getMessage)
.orElse(null);

View File

@ -1,6 +1,7 @@
package dev.vality.disputes.schedule;
import dev.vality.disputes.manualparsing.ManualParsingTopic;
import dev.vality.disputes.admin.callback.CallbackNotifier;
import dev.vality.disputes.admin.management.MdcTopicProducer;
import dev.vality.disputes.schedule.service.CreateAdjustmentsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -16,13 +17,15 @@ import org.springframework.stereotype.Service;
public class TaskReadyForCreateAdjustmentsService {
private final CreateAdjustmentsService createAdjustmentsService;
private final ManualParsingTopic manualParsingTopic;
private final CallbackNotifier callbackNotifier;
private final MdcTopicProducer mdcTopicProducer;
@Scheduled(fixedDelayString = "${dispute.fixedDelayReadyForCreateAdjustments}", initialDelayString = "${dispute.initialDelayReadyForCreateAdjustments}")
public void processPending() {
log.debug("Processing ReadyForCreateAdjustments get started");
var disputes = createAdjustmentsService.getReadyDisputesForCreateAdjustment();
manualParsingTopic.sendReadyForCreateAdjustments(disputes);
mdcTopicProducer.sendReadyForCreateAdjustments(disputes);
callbackNotifier.sendDisputeReadyForCreateAdjustment(disputes);
log.info("ReadyForCreateAdjustments were processed");
}
}

View File

@ -0,0 +1,46 @@
package dev.vality.disputes.schedule.catcher;
import dev.vality.disputes.schedule.model.ProviderData;
import dev.vality.disputes.schedule.service.ExternalGatewayChecker;
import dev.vality.woody.api.flow.error.WRuntimeException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.function.Consumer;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
public class WRuntimeExceptionCatcher {
private final ExternalGatewayChecker externalGatewayChecker;
public void catchProvidersDisputesApiNotExist(ProviderData providerData, Runnable runnable, Runnable defaultRemoteClientRunnable) {
try {
runnable.run();
} catch (WRuntimeException e) {
if (externalGatewayChecker.isProvidersDisputesApiNotExist(providerData, e)) {
// отправлять на ручной разбор, если API диспутов на провайдере не реализовано
// (тогда при тесте соединения вернется 404)
log.warn("Trying to call defaultRemoteClient.createDispute(), externalGatewayChecker", e);
defaultRemoteClientRunnable.run();
return;
}
throw e;
}
}
public void catchUnexpectedResultMapping(Runnable runnable, Consumer<WRuntimeException> unexpectedResultMappingHandler) {
try {
runnable.run();
} catch (WRuntimeException e) {
if (externalGatewayChecker.isProvidersDisputesUnexpectedResultMapping(e)) {
unexpectedResultMappingHandler.accept(e);
return;
}
throw e;
}
}
}

View File

@ -0,0 +1,16 @@
package dev.vality.disputes.schedule.client;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.provider.Attachment;
import dev.vality.disputes.provider.DisputeCreatedResult;
import dev.vality.disputes.schedule.model.ProviderData;
import java.util.List;
public interface DefaultRemoteClient {
Boolean routeUrlEquals(ProviderData providerData);
DisputeCreatedResult createDispute(Dispute dispute, List<Attachment> attachments, ProviderData providerData);
}

View File

@ -0,0 +1,46 @@
package dev.vality.disputes.schedule.client;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.provider.Attachment;
import dev.vality.disputes.provider.DisputeCreatedResult;
import dev.vality.disputes.schedule.converter.DisputeParamsConverter;
import dev.vality.disputes.schedule.model.ProviderData;
import dev.vality.disputes.service.external.DisputesTgBotService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.List;
@Slf4j
@Service
@ConditionalOnProperty(value = "service.disputes-tg-bot.provider.enabled", havingValue = "true", matchIfMissing = true)
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength"})
public class DisputesTgBotRemoteClientImpl implements DefaultRemoteClient {
private final DisputesTgBotService disputesTgBotService;
private final DisputeParamsConverter disputeParamsConverter;
@Value("${service.disputes-tg-bot.provider.url}")
private String routeUrl;
@Override
public Boolean routeUrlEquals(ProviderData providerData) {
return StringUtils.equalsIgnoreCase(providerData.getRouteUrl(), routeUrl);
}
@Override
public DisputeCreatedResult createDispute(Dispute dispute, List<Attachment> attachments, ProviderData providerData) {
log.debug("Trying to build disputeParams {}", dispute.getId());
var disputeParams = disputeParamsConverter.convert(dispute, attachments, providerData.getOptions());
providerData.setRouteUrl(routeUrl);
log.debug("Trying to disputesTgBotService.createDispute() call {}", dispute.getId());
var result = disputesTgBotService.createDispute(disputeParams);
log.info("disputesTgBotService.createDispute() has been called {} {}", dispute.getId(), result);
return result;
}
}

View File

@ -0,0 +1,37 @@
package dev.vality.disputes.schedule.client;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.provider.Attachment;
import dev.vality.disputes.provider.DisputeCreatedResult;
import dev.vality.disputes.provider.DisputeCreatedSuccessResult;
import dev.vality.disputes.schedule.model.ProviderData;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
@Slf4j
@Service
@ConditionalOnProperty(value = "service.disputes-tg-bot.provider.enabled", havingValue = "false")
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength"})
public class DummyRemoteClientImpl implements DefaultRemoteClient {
private final String routeUrl = "tg-bot";
@Override
public Boolean routeUrlEquals(ProviderData providerData) {
return StringUtils.equalsIgnoreCase(providerData.getRouteUrl(), routeUrl);
}
@Override
public DisputeCreatedResult createDispute(Dispute dispute, List<Attachment> attachments, ProviderData providerData) {
log.debug("Trying to call DummyRemoteClientImpl.createDispute() {}", dispute.getId());
providerData.setRouteUrl(routeUrl);
return DisputeCreatedResult.successResult(new DisputeCreatedSuccessResult(UUID.randomUUID().toString()));
}
}

View File

@ -9,6 +9,7 @@ import dev.vality.disputes.schedule.converter.DisputeContextConverter;
import dev.vality.disputes.schedule.converter.DisputeParamsConverter;
import dev.vality.disputes.schedule.model.ProviderData;
import dev.vality.disputes.schedule.service.ProviderIfaceBuilder;
import dev.vality.disputes.schedule.service.ProviderRouting;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@ -22,17 +23,18 @@ import java.util.List;
@SuppressWarnings({"ParameterName", "LineLength"})
public class RemoteClient {
private final ProviderRouting providerRouting;
private final ProviderIfaceBuilder providerIfaceBuilder;
private final DisputeContextConverter disputeContextConverter;
private final DisputeParamsConverter disputeParamsConverter;
private final DisputeContextConverter disputeContextConverter;
@SneakyThrows
public DisputeCreatedResult createDispute(Dispute dispute, List<Attachment> attachments, ProviderData providerData) {
log.debug("Trying to call dominant for RemoteClient {}", dispute.getId());
providerRouting.initRouteUrl(providerData);
log.debug("Trying to call ProviderIfaceBuilder {}", dispute.getId());
var remoteClient = providerIfaceBuilder.buildTHSpawnClient(providerData.getRouteUrl());
log.debug("Trying to build disputeParams {}", dispute.getId());
var disputeParams = disputeParamsConverter.convert(dispute, attachments, providerData.getOptions());
log.debug("Trying to call ProviderIfaceBuilder {}", dispute.getId());
var remoteClient = providerIfaceBuilder.buildTHSpawnClient(providerData);
log.debug("Trying to routed remote provider's createDispute() call {}", dispute.getId());
var result = remoteClient.createDispute(disputeParams);
log.info("Routed remote provider's createDispute() has been called {} {}", dispute.getId(), result);
@ -41,11 +43,11 @@ public class RemoteClient {
@SneakyThrows
public DisputeStatusResult checkDisputeStatus(Dispute dispute, ProviderDispute providerDispute, ProviderData providerData) {
log.debug("Trying to call dominant for RemoteClient {}", dispute.getId());
providerRouting.initRouteUrl(providerData);
log.debug("Trying to call ProviderIfaceBuilder {}", dispute.getId());
var remoteClient = providerIfaceBuilder.buildTHSpawnClient(providerData.getRouteUrl());
log.debug("Trying to build disputeContext {}", dispute.getId());
var disputeContext = disputeContextConverter.convert(dispute, providerDispute, providerData.getOptions());
log.debug("Trying to call ProviderIfaceBuilder {}", dispute.getId());
var remoteClient = providerIfaceBuilder.buildTHSpawnClient(providerData);
log.debug("Trying to routed remote provider's checkDisputeStatus() call {}", dispute.getId());
var result = remoteClient.checkDisputeStatus(disputeContext);
log.info("Routed remote provider's checkDisputeStatus() has been called {} {}", dispute.getId(), result);

View File

@ -0,0 +1,83 @@
package dev.vality.disputes.schedule.handler;
import dev.vality.disputes.admin.callback.CallbackNotifier;
import dev.vality.disputes.admin.management.MdcTopicProducer;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.dao.ProviderDisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.domain.tables.pojos.ProviderDispute;
import dev.vality.disputes.polling.ExponentialBackOffPollingServiceWrapper;
import dev.vality.disputes.provider.DisputeCreatedResult;
import dev.vality.disputes.schedule.client.DefaultRemoteClient;
import dev.vality.disputes.schedule.model.ProviderData;
import dev.vality.disputes.utils.ErrorFormatter;
import dev.vality.woody.api.flow.error.WRuntimeException;
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 static dev.vality.disputes.constant.ModerationPrefix.DISPUTES_UNKNOWN_MAPPING;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
public class DisputeCreateResultHandler {
private final DisputeDao disputeDao;
private final ExponentialBackOffPollingServiceWrapper exponentialBackOffPollingService;
private final DefaultRemoteClient defaultRemoteClient;
private final ProviderDisputeDao providerDisputeDao;
private final CallbackNotifier callbackNotifier;
private final MdcTopicProducer mdcTopicProducer;
@Transactional(propagation = Propagation.REQUIRED)
public void handleSuccessResult(Dispute dispute, DisputeCreatedResult result, ProviderData providerData) {
var nextCheckAfter = exponentialBackOffPollingService.prepareNextPollingInterval(dispute, providerData.getOptions());
providerDisputeDao.save(new ProviderDispute(result.getSuccessResult().getProviderDisputeId(), dispute.getId()));
log.info("Trying to set pending Dispute status {}, {}", dispute, result);
var isDefaultRouteUrl = defaultRemoteClient.routeUrlEquals(providerData);
disputeDao.update(dispute.getId(), !isDefaultRouteUrl ? DisputeStatus.pending : DisputeStatus.manual_pending, nextCheckAfter);
log.debug("Dispute status has been set to pending {}", dispute.getId());
}
@Transactional(propagation = Propagation.REQUIRED)
public void handleFailResult(Dispute dispute, DisputeCreatedResult result) {
var failure = result.getFailResult().getFailure();
var errorMessage = ErrorFormatter.getErrorMessage(failure);
if (errorMessage.startsWith(DISPUTES_UNKNOWN_MAPPING)) {
handleUnexpectedResultMapping(dispute, failure.getCode(), failure.getReason());
} else {
log.warn("Trying to set failed Dispute status {}, {}", dispute.getId(), errorMessage);
disputeDao.update(dispute.getId(), DisputeStatus.failed, errorMessage, failure.getCode());
log.debug("Dispute status has been set to failed {}", dispute.getId());
}
}
@Transactional(propagation = Propagation.REQUIRED)
public void handleAlreadyExistResult(Dispute dispute) {
callbackNotifier.sendDisputeAlreadyCreated(dispute);
mdcTopicProducer.sendCreated(dispute, DisputeStatus.already_exist_created, "dispute already exist");
log.info("Trying to set {} Dispute status {}", DisputeStatus.already_exist_created, dispute);
disputeDao.update(dispute.getId(), DisputeStatus.already_exist_created);
log.debug("Dispute status has been set to {} {}", DisputeStatus.already_exist_created, dispute.getId());
}
@Transactional(propagation = Propagation.REQUIRED)
public void handleUnexpectedResultMapping(Dispute dispute, WRuntimeException e) {
var errorMessage = e.getErrorDefinition().getErrorReason();
handleUnexpectedResultMapping(dispute, errorMessage, null);
}
private void handleUnexpectedResultMapping(Dispute dispute, String errorCode, String errorDescription) {
callbackNotifier.sendDisputeFailedReviewRequired(dispute, errorCode, errorDescription);
var errorMessage = ErrorFormatter.getErrorMessage(errorCode, errorDescription);
mdcTopicProducer.sendCreated(dispute, DisputeStatus.manual_created, errorMessage);
log.warn("Trying to set manual_created Dispute status {}, {}", dispute.getId(), errorMessage);
disputeDao.update(dispute.getId(), DisputeStatus.manual_created, errorMessage);
log.debug("Dispute status has been set to manual_created {}", dispute.getId());
}
}

View File

@ -1,19 +1,26 @@
package dev.vality.disputes.schedule.handler;
import dev.vality.disputes.admin.callback.CallbackNotifier;
import dev.vality.disputes.admin.management.MdcTopicProducer;
import dev.vality.disputes.constant.ErrorReason;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.polling.ExponentialBackOffPollingServiceWrapper;
import dev.vality.disputes.provider.DisputeStatusResult;
import dev.vality.disputes.utils.ErrorFormatter;
import dev.vality.woody.api.flow.error.WRuntimeException;
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.List;
import java.util.Map;
import static dev.vality.disputes.constant.ModerationPrefix.DISPUTES_UNKNOWN_MAPPING;
@Slf4j
@Service
@RequiredArgsConstructor
@ -22,6 +29,8 @@ public class DisputeStatusResultHandler {
private final DisputeDao disputeDao;
private final ExponentialBackOffPollingServiceWrapper exponentialBackOffPollingService;
private final CallbackNotifier callbackNotifier;
private final MdcTopicProducer mdcTopicProducer;
@Transactional(propagation = Propagation.REQUIRED)
public void handleStatusPending(Dispute dispute, DisputeStatusResult result, Map<String, String> options) {
@ -36,17 +45,48 @@ public class DisputeStatusResultHandler {
@Transactional(propagation = Propagation.REQUIRED)
public void handleStatusFail(Dispute dispute, DisputeStatusResult result) {
var errorMessage = ErrorFormatter.getErrorMessage(result.getStatusFail().getFailure());
var failure = result.getStatusFail().getFailure();
var errorMessage = ErrorFormatter.getErrorMessage(failure);
if (errorMessage.startsWith(DISPUTES_UNKNOWN_MAPPING)) {
handleUnexpectedResultMapping(dispute, failure.getCode(), failure.getReason());
} else {
log.warn("Trying to set failed Dispute status {}, {}", dispute.getId(), errorMessage);
disputeDao.update(dispute.getId(), DisputeStatus.failed, errorMessage);
disputeDao.update(dispute.getId(), DisputeStatus.failed, errorMessage, failure.getCode());
log.debug("Dispute status has been set to failed {}", dispute.getId());
}
}
@Transactional(propagation = Propagation.REQUIRED)
public void handleStatusSuccess(Dispute dispute, DisputeStatusResult result) {
callbackNotifier.sendDisputeReadyForCreateAdjustment(List.of(dispute));
mdcTopicProducer.sendReadyForCreateAdjustments(List.of(dispute));
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.getId());
}
@Transactional(propagation = Propagation.REQUIRED)
public void handlePoolingExpired(Dispute dispute) {
callbackNotifier.sendDisputePoolingExpired(dispute);
mdcTopicProducer.sendPoolingExpired(dispute);
log.warn("Trying to set manual_pending Dispute status with POOLING_EXPIRED error reason {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.manual_pending, ErrorReason.POOLING_EXPIRED);
log.debug("Dispute status has been set to manual_pending {}", dispute.getId());
}
@Transactional(propagation = Propagation.REQUIRED)
public void handleUnexpectedResultMapping(Dispute dispute, WRuntimeException e) {
var errorMessage = e.getErrorDefinition().getErrorReason();
handleUnexpectedResultMapping(dispute, errorMessage, null);
}
private void handleUnexpectedResultMapping(Dispute dispute, String errorCode, String errorDescription) {
callbackNotifier.sendDisputeFailedReviewRequired(dispute, errorCode, errorDescription);
var errorMessage = ErrorFormatter.getErrorMessage(errorCode, errorDescription);
mdcTopicProducer.sendCreated(dispute, DisputeStatus.manual_pending, errorMessage);
log.warn("Trying to set manual_pending Dispute status {}, {}", dispute.getId(), errorMessage);
disputeDao.update(dispute.getId(), DisputeStatus.manual_pending, errorMessage);
log.debug("Dispute status has been set to manual_pending {}", dispute.getId());
}
}

View File

@ -11,5 +11,6 @@ public class ProviderData {
private Map<String, String> options;
private String defaultProviderUrl;
private String routeUrl;
}

View File

@ -3,18 +3,15 @@ package dev.vality.disputes.schedule.service;
import dev.vality.damsel.payment_processing.InvoicePayment;
import dev.vality.disputes.constant.ErrorReason;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.dao.ProviderDisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.domain.tables.pojos.ProviderDispute;
import dev.vality.disputes.manualparsing.ManualParsingTopic;
import dev.vality.disputes.polling.ExponentialBackOffPollingServiceWrapper;
import dev.vality.disputes.provider.Attachment;
import dev.vality.disputes.provider.DisputeCreatedResult;
import dev.vality.disputes.schedule.catcher.WRuntimeExceptionCatcher;
import dev.vality.disputes.schedule.client.DefaultRemoteClient;
import dev.vality.disputes.schedule.client.RemoteClient;
import dev.vality.disputes.schedule.handler.DisputeCreateResultHandler;
import dev.vality.disputes.schedule.model.ProviderData;
import dev.vality.disputes.service.external.InvoicingService;
import dev.vality.disputes.utils.ErrorFormatter;
import dev.vality.woody.api.flow.error.WRuntimeException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -31,18 +28,17 @@ import static dev.vality.disputes.constant.TerminalOptionsField.DISPUTE_FLOW_PRO
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
@SuppressWarnings({"MemberName", "ParameterName", "LineLength", "MissingSwitchDefault"})
public class CreatedDisputesService {
private final RemoteClient remoteClient;
private final DisputeDao disputeDao;
private final ProviderDisputeDao providerDisputeDao;
private final CreatedAttachmentsService createdAttachmentsService;
private final InvoicingService invoicingService;
private final ExponentialBackOffPollingServiceWrapper exponentialBackOffPollingService;
private final ProviderDataService providerDataService;
private final ExternalGatewayChecker externalGatewayChecker;
private final ManualParsingTopic manualParsingTopic;
private final DefaultRemoteClient defaultRemoteClient;
private final DisputeCreateResultHandler disputeCreateResultHandler;
private final WRuntimeExceptionCatcher wRuntimeExceptionCatcher;
@Transactional(propagation = Propagation.REQUIRED)
public List<Dispute> getCreatedDisputesForUpdateSkipLocked(int batchSize) {
@ -87,52 +83,38 @@ public class CreatedDisputesService {
|| isNotProvidersDisputesApiExist(options)) {
// отправлять на ручной разбор, если выставлена опция
// DISPUTE_FLOW_CAPTURED_BLOCKED или не выставлена DISPUTE_FLOW_PROVIDERS_API_EXIST
log.warn("finishTaskWithManualParsingFlowActivation, options capt={}, apiExist={}", isCapturedBlockedForDispute(options), isNotProvidersDisputesApiExist(options));
finishTaskWithManualParsingFlowActivation(dispute, attachments, DisputeStatus.manual_created);
log.warn("Trying to call defaultRemoteClient.createDispute(), options capt={}, apiExist={}", isCapturedBlockedForDispute(options), isNotProvidersDisputesApiExist(options));
wRuntimeExceptionCatcher.catchUnexpectedResultMapping(
() -> {
var result = defaultRemoteClient.createDispute(dispute, attachments, providerData);
finishTask(dispute, result, providerData);
},
e -> disputeCreateResultHandler.handleUnexpectedResultMapping(dispute, e));
return;
}
try {
wRuntimeExceptionCatcher.catchUnexpectedResultMapping(
() -> wRuntimeExceptionCatcher.catchProvidersDisputesApiNotExist(
providerData,
() -> {
var result = remoteClient.createDispute(dispute, attachments, providerData);
finishTask(dispute, attachments, result, options);
} catch (WRuntimeException e) {
if (externalGatewayChecker.isNotProvidersDisputesApiExist(providerData, e)) {
// отправлять на ручной разбор, если API диспутов на провайдере не реализовано
// (тогда при тесте соединения вернется 404)
log.warn("finishTaskWithManualParsingFlowActivation with externalGatewayChecker", e);
finishTaskWithManualParsingFlowActivation(dispute, attachments, DisputeStatus.manual_created);
return;
}
throw e;
}
finishTask(dispute, result, providerData);
},
() -> wRuntimeExceptionCatcher.catchUnexpectedResultMapping(
() -> {
var result = defaultRemoteClient.createDispute(dispute, attachments, providerData);
finishTask(dispute, result, providerData);
},
e -> disputeCreateResultHandler.handleUnexpectedResultMapping(dispute, e))),
e -> disputeCreateResultHandler.handleUnexpectedResultMapping(dispute, e));
}
@Transactional(propagation = Propagation.REQUIRED)
void finishTask(Dispute dispute, List<Attachment> attachments, DisputeCreatedResult result, Map<String, String> options) {
void finishTask(Dispute dispute, DisputeCreatedResult result, ProviderData providerData) {
switch (result.getSetField()) {
case SUCCESS_RESULT -> {
var nextCheckAfter = exponentialBackOffPollingService.prepareNextPollingInterval(dispute, options);
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.getId());
case SUCCESS_RESULT -> disputeCreateResultHandler.handleSuccessResult(dispute, result, providerData);
case FAIL_RESULT -> disputeCreateResultHandler.handleFailResult(dispute, result);
case ALREADY_EXIST_RESULT -> disputeCreateResultHandler.handleAlreadyExistResult(dispute);
}
case FAIL_RESULT -> {
var errorMessage = ErrorFormatter.getErrorMessage(result.getFailResult().getFailure());
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.getId());
}
case ALREADY_EXIST_RESULT ->
finishTaskWithManualParsingFlowActivation(dispute, attachments, DisputeStatus.already_exist_created);
}
}
@Transactional(propagation = Propagation.REQUIRED)
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());
}
private boolean isCapturedBlockedForDispute(Map<String, String> options) {

View File

@ -22,23 +22,32 @@ public class ExternalGatewayChecker {
private final CloseableHttpClient httpClient;
private final ProviderRouting providerRouting;
public boolean isNotProvidersDisputesApiExist(ProviderData providerData, WRuntimeException e) {
public boolean isProvidersDisputesUnexpectedResultMapping(WRuntimeException e) {
return e.getErrorDefinition() != null
&& e.getErrorDefinition().getGenerationSource() == WErrorSource.EXTERNAL
&& e.getErrorDefinition().getErrorType() == WErrorType.UNEXPECTED_ERROR
&& e.getErrorDefinition().getErrorSource() == WErrorSource.INTERNAL
&& isNotFoundProvidersDisputesApi(providerData);
&& e.getErrorDefinition().getErrorReason() != null
&& e.getErrorDefinition().getErrorReason().contains("Unexpected result, code = ");
}
public boolean isProvidersDisputesApiNotExist(ProviderData providerData, WRuntimeException e) {
return e.getErrorDefinition() != null
&& e.getErrorDefinition().getGenerationSource() == WErrorSource.EXTERNAL
&& e.getErrorDefinition().getErrorType() == WErrorType.UNEXPECTED_ERROR
&& e.getErrorDefinition().getErrorSource() == WErrorSource.INTERNAL
&& isProvidersDisputesApiNotFound(providerData);
}
@SneakyThrows
private Boolean isNotFoundProvidersDisputesApi(ProviderData providerData) {
private Boolean isProvidersDisputesApiNotFound(ProviderData providerData) {
return httpClient.execute(new HttpGet(getRouteUrl(providerData)), isNotFoundResponse());
}
private String getRouteUrl(ProviderData providerData) {
var routeUrl = providerRouting.getRouteUrl(providerData);
log.debug("Check adapter connection, routeUrl={}", routeUrl);
return routeUrl;
providerRouting.initRouteUrl(providerData);
log.debug("Check adapter connection, routeUrl={}", providerData.getRouteUrl());
return providerData.getRouteUrl();
}
private HttpClientResponseHandler<Boolean> isNotFoundResponse() {

View File

@ -1,14 +1,13 @@
package dev.vality.disputes.schedule.service;
import dev.vality.disputes.constant.ErrorReason;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.dao.ProviderDisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.manualparsing.ManualParsingTopic;
import dev.vality.disputes.polling.ExponentialBackOffPollingServiceWrapper;
import dev.vality.disputes.polling.PollingInfoService;
import dev.vality.disputes.provider.DisputeStatusResult;
import dev.vality.disputes.schedule.catcher.WRuntimeExceptionCatcher;
import dev.vality.disputes.schedule.client.RemoteClient;
import dev.vality.disputes.schedule.handler.DisputeStatusResultHandler;
import lombok.RequiredArgsConstructor;
@ -24,7 +23,7 @@ import java.util.Map;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
@SuppressWarnings({"MemberName", "ParameterName", "LineLength", "MissingSwitchDefault"})
public class PendingDisputesService {
private final RemoteClient remoteClient;
@ -34,7 +33,7 @@ public class PendingDisputesService {
private final ExponentialBackOffPollingServiceWrapper exponentialBackOffPollingService;
private final ProviderDataService providerDataService;
private final DisputeStatusResultHandler disputeStatusResultHandler;
private final ManualParsingTopic manualParsingTopic;
private final WRuntimeExceptionCatcher wRuntimeExceptionCatcher;
@Transactional(propagation = Propagation.REQUIRED)
public List<Dispute> getPendingDisputesForUpdateSkipLocked(int batchSize) {
@ -65,15 +64,16 @@ public class PendingDisputesService {
return;
}
if (pollingInfoService.isDeadline(dispute)) {
manualParsingTopic.sendPoolingExpired(dispute);
log.error("Trying to set manual_pending Dispute status with POOLING_EXPIRED error reason {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.manual_pending, ErrorReason.POOLING_EXPIRED);
log.debug("Dispute status has been set to manual_pending {}", dispute.getId());
disputeStatusResultHandler.handlePoolingExpired(dispute);
return;
}
log.debug("ProviderDispute has been found {}", dispute.getId());
wRuntimeExceptionCatcher.catchUnexpectedResultMapping(
() -> {
var result = remoteClient.checkDisputeStatus(dispute, providerDispute, providerData);
finishTask(dispute, result, providerData.getOptions());
},
e -> disputeStatusResultHandler.handleUnexpectedResultMapping(dispute, e));
}
@Transactional(propagation = Propagation.REQUIRED)

View File

@ -2,7 +2,6 @@ package dev.vality.disputes.schedule.service;
import dev.vality.disputes.config.properties.AdaptersConnectionProperties;
import dev.vality.disputes.provider.ProviderDisputesServiceSrv;
import dev.vality.disputes.schedule.model.ProviderData;
import dev.vality.woody.thrift.impl.http.THSpawnClientBuilder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -18,12 +17,10 @@ import java.util.concurrent.TimeUnit;
@SuppressWarnings({"AbbreviationAsWordInName", "LineLength"})
public class ProviderIfaceBuilder {
private final ProviderRouting providerRouting;
private final AdaptersConnectionProperties adaptersConnectionProperties;
@Cacheable(value = "adapters", key = "#providerData.defaultProviderUrl", cacheManager = "adaptersCacheManager")
public ProviderDisputesServiceSrv.Iface buildTHSpawnClient(ProviderData providerData) {
var routeUrl = providerRouting.getRouteUrl(providerData);
@Cacheable(value = "adapters", key = "#root.args[0]", cacheManager = "adaptersCacheManager")
public ProviderDisputesServiceSrv.Iface buildTHSpawnClient(String routeUrl) {
log.info("Creating new client for url: {}", routeUrl);
return new THSpawnClientBuilder()
.withNetworkTimeout((int) TimeUnit.SECONDS.toMillis(adaptersConnectionProperties.getTimeoutSec()))

View File

@ -18,12 +18,12 @@ public class ProviderRouting {
private static final String DISPUTES_URL_POSTFIX_DEFAULT = "disputes";
private static final String OPTION_DISPUTES_URL_FIELD_NAME = "disputes_url";
public String getRouteUrl(ProviderData providerData) {
public void initRouteUrl(ProviderData providerData) {
var url = providerData.getOptions().get(OPTION_DISPUTES_URL_FIELD_NAME);
if (ObjectUtils.isEmpty(url)) {
url = createDefaultRouteUrl(providerData.getDefaultProviderUrl());
}
return url;
providerData.setRouteUrl(url);
}
private String createDefaultRouteUrl(String defaultProviderUrl) {

View File

@ -0,0 +1,24 @@
package dev.vality.disputes.service.external;
import dev.vality.disputes.admin.DisputeAlreadyCreated;
import dev.vality.disputes.admin.DisputeFailedReviewRequired;
import dev.vality.disputes.admin.DisputePoolingExpired;
import dev.vality.disputes.admin.DisputeReadyForCreateAdjustment;
import dev.vality.disputes.provider.DisputeCreatedResult;
import dev.vality.disputes.provider.DisputeParams;
import java.util.List;
public interface DisputesTgBotService {
DisputeCreatedResult createDispute(DisputeParams disputeParams);
void sendDisputeAlreadyCreated(DisputeAlreadyCreated disputeAlreadyCreated);
void sendDisputePoolingExpired(DisputePoolingExpired disputePoolingExpired);
void sendDisputeReadyForCreateAdjustment(List<DisputeReadyForCreateAdjustment> disputeReadyForCreateAdjustments);
void sendDisputeFailedReviewRequired(DisputeFailedReviewRequired disputeFailedReviewRequired);
}

View File

@ -0,0 +1,75 @@
package dev.vality.disputes.service.external.impl;
import dev.vality.disputes.admin.*;
import dev.vality.disputes.provider.DisputeCreatedResult;
import dev.vality.disputes.provider.DisputeParams;
import dev.vality.disputes.provider.ProviderDisputesServiceSrv;
import dev.vality.disputes.service.external.DisputesTgBotService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings({"LineLength"})
public class DisputesTgBotServiceImpl implements DisputesTgBotService {
public final ProviderDisputesServiceSrv.Iface providerDisputesTgBotClient;
public final AdminCallbackServiceSrv.Iface adminCallbackDisputesTgBotClient;
@Override
@SneakyThrows
public DisputeCreatedResult createDispute(DisputeParams disputeParams) {
log.debug("Trying to call providerDisputesTgBotClient.createDispute() {} {}", disputeParams.getDisputeId(), disputeParams.getTransactionContext().getInvoiceId());
var invoice = providerDisputesTgBotClient.createDispute(disputeParams);
log.debug("providerDisputesTgBotClient.createDispute() has been called {} {}", disputeParams.getDisputeId(), disputeParams.getTransactionContext().getInvoiceId());
return invoice;
}
@Override
@SneakyThrows
public void sendDisputeAlreadyCreated(DisputeAlreadyCreated disputeAlreadyCreated) {
log.debug("Trying to call adminCallbackDisputesTgBotClient.sendDisputeAlreadyCreated() {}", disputeAlreadyCreated.getId());
adminCallbackDisputesTgBotClient.notify(
new NotificationParamsRequest(List.of(Notification.disputeAlreadyCreated(disputeAlreadyCreated))));
log.debug("adminCallbackDisputesTgBotClient.sendDisputeAlreadyCreated() has been called {}", disputeAlreadyCreated.getId());
}
@Override
@SneakyThrows
public void sendDisputePoolingExpired(DisputePoolingExpired disputePoolingExpired) {
log.debug("Trying to call adminCallbackDisputesTgBotClient.sendDisputePoolingExpired() {}", disputePoolingExpired.getId());
adminCallbackDisputesTgBotClient.notify(
new NotificationParamsRequest(List.of(Notification.disputePoolingExpired(disputePoolingExpired))));
log.debug("adminCallbackDisputesTgBotClient.sendDisputePoolingExpired() has been called {}", disputePoolingExpired.getId());
}
@Override
@SneakyThrows
public void sendDisputeReadyForCreateAdjustment(List<DisputeReadyForCreateAdjustment> disputeReadyForCreateAdjustments) {
var ids = disputeReadyForCreateAdjustments.stream()
.map(DisputeReadyForCreateAdjustment::getId)
.map(String::valueOf)
.collect(Collectors.joining(", "));
log.debug("Trying to call adminCallbackDisputesTgBotClient.sendDisputeReadyForCreateAdjustment() {}", ids);
var notifications = disputeReadyForCreateAdjustments.stream()
.map(Notification::disputeReadyForCreateAdjustment)
.collect(Collectors.toList());
adminCallbackDisputesTgBotClient.notify(new NotificationParamsRequest(notifications));
log.debug("adminCallbackDisputesTgBotClient.sendDisputeReadyForCreateAdjustment() has been called {}", ids);
}
@Override
@SneakyThrows
public void sendDisputeFailedReviewRequired(DisputeFailedReviewRequired disputeFailedReviewRequired) {
log.debug("Trying to call adminCallbackDisputesTgBotClient.sendDisputeFailedReviewRequired() {}", disputeFailedReviewRequired.getId());
adminCallbackDisputesTgBotClient.notify(
new NotificationParamsRequest(List.of(Notification.disputeFailedReviewRequired(disputeFailedReviewRequired))));
log.debug("adminCallbackDisputesTgBotClient.sendDisputeFailedReviewRequired() has been called {}", disputeFailedReviewRequired.getId());
}
}

View File

@ -1,6 +1,6 @@
package dev.vality.disputes.servlet;
import dev.vality.disputes.admin.ManualParsingServiceSrv;
import dev.vality.disputes.admin.AdminManagementServiceSrv;
import dev.vality.woody.thrift.impl.http.THServiceBuilder;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebServlet;
@ -8,11 +8,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
@WebServlet("/disputes-api/v1/manual-parsing")
public class ManualParsingServlet extends GenericServlet {
@WebServlet("/disputes-api/v1/admin-management")
public class AdminManagementServlet extends GenericServlet {
@Autowired
private ManualParsingServiceSrv.Iface manualParsingHandler;
private AdminManagementServiceSrv.Iface adminManagementHandler;
private Servlet servlet;
@ -20,7 +20,7 @@ public class ManualParsingServlet extends GenericServlet {
public void init(ServletConfig config) throws ServletException {
super.init(config);
servlet = new THServiceBuilder()
.build(ManualParsingServiceSrv.Iface.class, manualParsingHandler);
.build(AdminManagementServiceSrv.Iface.class, adminManagementHandler);
}
@Override

View File

@ -12,4 +12,11 @@ public class ErrorFormatter {
}
return TErrorUtil.toStringVal(failure);
}
public static String getErrorMessage(String errorCode, String errorDescription) {
if (!StringUtils.isBlank(errorDescription)) {
return errorCode + ": " + errorDescription;
}
return errorCode;
}
}

View File

@ -64,6 +64,17 @@ service:
shops:
poolSize: 10
ttlSec: 86400
disputes-tg-bot:
provider:
url: http://localhost:8022/change_it
networkTimeout: 5000
enabled: false
admin:
url: http://localhost:8022/change_it
networkTimeout: 5000
enabled: false
mdc-topic-producer:
enabled: true
adapters:
connection:
timeoutSec: 30
@ -106,18 +117,15 @@ dispute:
isScheduleCreatedEnabled: true
isSchedulePendingEnabled: true
isScheduleCreateAdjustmentsEnabled: true
isScheduleReadyForCreateAdjustmentsEnabled: true
isScheduleReadyForCreateAdjustmentsEnabled: false
time:
config:
max-time-polling-min: 600
manual-parsing-topic:
enabled: true
testcontainers:
postgresql:
tag: '11.4'
tag: '14.12'
http-client:
requestTimeout: 60000
@ -125,3 +133,7 @@ http-client:
connectionTimeout: 10000
maxTotalPooling: 200
defaultMaxPerRoute: 200
otel:
resource: http://localhost:4318/v1/traces
timeout: 60000

View File

@ -0,0 +1,2 @@
ALTER TABLE dspt.dispute
ADD COLUMN "mapping" CHARACTER VARYING;

View File

@ -1,4 +1,4 @@
package dev.vality.disputes.manualparsing;
package dev.vality.disputes.admin.management;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
@ -16,10 +16,8 @@ import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.ArrayList;
@ -28,43 +26,49 @@ import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping({"/debug/disputes-api/manual-parsing"})
@RequestMapping({"/debug/disputes-api/admin-management"})
@Slf4j
public class DebugManualParsingController {
public class DebugAdminManagementController {
private final ManualParsingServiceSrv.Iface manualParsingHandler;
private final AdminManagementServiceSrv.Iface adminManagementHandler;
private final ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jdk8Module());
@PostMapping("/cancel")
@SneakyThrows
public void cancelPending(@RequestBody String body) {
log.debug("cancelPending {}", body);
manualParsingHandler.cancelPending(objectMapper.readValue(body, CancelParamsRequest.class));
adminManagementHandler.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));
adminManagementHandler.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));
adminManagementHandler.bindCreated(objectMapper.readValue(body, BindParamsRequest.class));
}
@PostMapping("/get")
@SneakyThrows
public DisputeResult getDisputes(@RequestBody String body) {
log.debug("getDispute {}", body);
var dispute = manualParsingHandler.getDisputes(objectMapper.readValue(body, DisputeParamsRequest.class));
var dispute = adminManagementHandler.getDisputes(objectMapper.readValue(body, DisputeParamsRequest.class));
return objectMapper.convertValue(dispute, new TypeReference<>() {
});
}
@GetMapping("/disputes")
@ResponseStatus(HttpStatus.NOT_FOUND)
public void defaultRouteUrl() {
log.info("hi");
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)

View File

@ -1,9 +1,9 @@
package dev.vality.disputes.manualparsing;
package dev.vality.disputes.admin.management;
import dev.vality.disputes.admin.AdminManagementServiceSrv;
import dev.vality.disputes.admin.Attachment;
import dev.vality.disputes.admin.Dispute;
import dev.vality.disputes.admin.DisputeResult;
import dev.vality.disputes.admin.ManualParsingServiceSrv;
import dev.vality.disputes.config.SpringBootUTest;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
@ -19,17 +19,17 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
@SpringBootUTest
public class DebugManualParsingControllerTest {
public class DebugAdminManagementControllerTest {
@MockBean
private ManualParsingServiceSrv.Iface manualParsingHandler;
private AdminManagementServiceSrv.Iface adminManagementHandler;
@Autowired
private DebugManualParsingController debugManualParsingController;
private DebugAdminManagementController debugAdminManagementController;
@Test
@SneakyThrows
public void checkSerialization() {
debugManualParsingController.approvePending("""
debugAdminManagementController.approvePending("""
{
"approveParams": [
{
@ -39,7 +39,7 @@ public class DebugManualParsingControllerTest {
]
}
""");
debugManualParsingController.cancelPending("""
debugAdminManagementController.cancelPending("""
{
"cancelParams": [
{
@ -49,7 +49,7 @@ public class DebugManualParsingControllerTest {
]
}
""");
debugManualParsingController.cancelPending("""
debugAdminManagementController.cancelPending("""
{
"cancelParams": [
{
@ -59,7 +59,7 @@ public class DebugManualParsingControllerTest {
]
}
""");
debugManualParsingController.bindCreated("""
debugAdminManagementController.bindCreated("""
{
"bindParams": [
{
@ -77,9 +77,9 @@ public class DebugManualParsingControllerTest {
randomed.setDisputes(List.of(
randomThrift(Dispute.class).setAttachments(List.of(new Attachment().setData(b))),
randomThrift(Dispute.class).setAttachments(List.of(new Attachment().setData(a)))));
given(manualParsingHandler.getDisputes(any()))
given(adminManagementHandler.getDisputes(any()))
.willReturn(randomed);
var disputes = debugManualParsingController.getDisputes("""
var disputes = debugAdminManagementController.getDisputes("""
{
"disputeParams": [
{

View File

@ -1,4 +1,4 @@
package dev.vality.disputes.manualparsing;
package dev.vality.disputes.admin.management;
import dev.vality.disputes.config.WireMockSpringBootITest;
import dev.vality.disputes.dao.DisputeDao;
@ -20,7 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
@WireMockSpringBootITest
@Import({PendingDisputesTestService.class})
public class DebugManualParsingHandlerTest {
public class DebugAdminManagementHandlerTest {
@Autowired
private DisputeDao disputeDao;
@ -31,19 +31,19 @@ public class DebugManualParsingHandlerTest {
@Autowired
private PendingDisputesTestService pendingDisputesTestService;
@Autowired
private DebugManualParsingController debugManualParsingController;
private DebugAdminManagementController debugAdminManagementController;
@Test
public void testCancelCreateAdjustment() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
debugManualParsingController.cancelPending(getCancelRequest(disputeId.toString()));
debugAdminManagementController.cancelPending(getCancelRequest(disputeId.toString()));
assertEquals(DisputeStatus.cancelled, disputeDao.get(disputeId).get().getStatus());
}
@Test
public void testCancelPending() {
var disputeId = createdDisputesTestService.callCreateDisputeRemotely();
debugManualParsingController.cancelPending(getCancelRequest(disputeId.toString()));
debugAdminManagementController.cancelPending(getCancelRequest(disputeId.toString()));
assertEquals(DisputeStatus.cancelled, disputeDao.get(disputeId).get().getStatus());
}
@ -51,14 +51,14 @@ public class DebugManualParsingHandlerTest {
public void testCancelFailed() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
disputeDao.update(disputeId, DisputeStatus.failed);
debugManualParsingController.cancelPending(getCancelRequest(disputeId.toString()));
debugAdminManagementController.cancelPending(getCancelRequest(disputeId.toString()));
assertEquals(DisputeStatus.failed, disputeDao.get(disputeId).get().getStatus());
}
@Test
public void testApproveCreateAdjustmentWithCallHg() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
debugManualParsingController.approvePending(getApproveRequest(disputeId.toString(), false));
debugAdminManagementController.approvePending(getApproveRequest(disputeId.toString(), false));
assertEquals(DisputeStatus.create_adjustment, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@ -66,7 +66,7 @@ public class DebugManualParsingHandlerTest {
@Test
public void testApproveCreateAdjustmentWithSkipHg() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
debugManualParsingController.approvePending(getApproveRequest(disputeId.toString(), true));
debugAdminManagementController.approvePending(getApproveRequest(disputeId.toString(), true));
assertEquals(DisputeStatus.succeeded, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@ -74,7 +74,7 @@ public class DebugManualParsingHandlerTest {
@Test
public void testApprovePendingWithSkipHg() {
var disputeId = createdDisputesTestService.callCreateDisputeRemotely();
debugManualParsingController.approvePending(getApproveRequest(disputeId.toString(), true));
debugAdminManagementController.approvePending(getApproveRequest(disputeId.toString(), true));
assertEquals(DisputeStatus.succeeded, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@ -83,7 +83,7 @@ public class DebugManualParsingHandlerTest {
public void testApproveFailed() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
disputeDao.update(disputeId, DisputeStatus.failed);
debugManualParsingController.approvePending(getApproveRequest(disputeId.toString(), true));
debugAdminManagementController.approvePending(getApproveRequest(disputeId.toString(), true));
assertEquals(DisputeStatus.failed, disputeDao.get(disputeId).get().getStatus());
}
@ -91,7 +91,7 @@ public class DebugManualParsingHandlerTest {
public void testBindCreatedCreateAdjustment() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
var providerDisputeId = generateId();
debugManualParsingController.bindCreated(getBindCreatedRequest(disputeId.toString(), providerDisputeId));
debugAdminManagementController.bindCreated(getBindCreatedRequest(disputeId.toString(), providerDisputeId));
assertEquals(DisputeStatus.create_adjustment, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@ -100,7 +100,7 @@ public class DebugManualParsingHandlerTest {
public void testBindCreatedPending() {
var disputeId = createdDisputesTestService.callCreateDisputeRemotely();
var providerDisputeId = generateId();
debugManualParsingController.bindCreated(getBindCreatedRequest(disputeId.toString(), providerDisputeId));
debugAdminManagementController.bindCreated(getBindCreatedRequest(disputeId.toString(), providerDisputeId));
assertEquals(DisputeStatus.pending, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@ -112,7 +112,7 @@ public class DebugManualParsingHandlerTest {
var providerDisputeId = generateId();
var disputeId = disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId();
disputeDao.update(UUID.fromString(disputeId), DisputeStatus.manual_created);
debugManualParsingController.bindCreated(getBindCreatedRequest(disputeId, providerDisputeId));
debugAdminManagementController.bindCreated(getBindCreatedRequest(disputeId, providerDisputeId));
assertEquals(DisputeStatus.manual_pending, disputeDao.get(UUID.fromString(disputeId)).get().getStatus());
disputeDao.update(UUID.fromString(disputeId), DisputeStatus.failed);
}
@ -124,7 +124,7 @@ public class DebugManualParsingHandlerTest {
var providerDisputeId = generateId();
var disputeId = disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId();
disputeDao.update(UUID.fromString(disputeId), DisputeStatus.already_exist_created);
debugManualParsingController.bindCreated(getBindCreatedRequest(disputeId, providerDisputeId));
debugAdminManagementController.bindCreated(getBindCreatedRequest(disputeId, providerDisputeId));
assertEquals(DisputeStatus.pending, disputeDao.get(UUID.fromString(disputeId)).get().getStatus());
disputeDao.update(UUID.fromString(disputeId), DisputeStatus.failed);
}
@ -134,7 +134,7 @@ public class DebugManualParsingHandlerTest {
public void testGetDispute() {
WiremockUtils.mockS3AttachmentDownload();
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
var disputes = debugManualParsingController.getDisputes(getGetDisputeRequest(disputeId.toString(), true));
var disputes = debugAdminManagementController.getDisputes(getGetDisputeRequest(disputeId.toString(), true));
assertEquals(1, disputes.getDisputes().size());
disputeDao.update(disputeId, DisputeStatus.failed);
}

View File

@ -1,8 +1,8 @@
package dev.vality.disputes.api;
import dev.vality.damsel.payment_processing.InvoicingSrv;
import dev.vality.disputes.admin.AdminManagementServiceSrv;
import dev.vality.disputes.admin.CancelParamsRequest;
import dev.vality.disputes.admin.ManualParsingServiceSrv;
import dev.vality.disputes.callback.DisputeCallbackParams;
import dev.vality.disputes.callback.ProviderDisputesCallbackServiceSrv;
import dev.vality.disputes.config.WireMockSpringBootITest;
@ -53,11 +53,11 @@ public class ServletTest {
@Test
@SneakyThrows
public void manualServletTest() {
public void adminManagementServletTest() {
var iface = new THSpawnClientBuilder()
.withAddress(new URI("http://127.0.0.1:" + serverPort + MANUAL))
.withAddress(new URI("http://127.0.0.1:" + serverPort + ADMIN_MANAGEMENT))
.withNetworkTimeout(5000)
.build(ManualParsingServiceSrv.Iface.class);
.build(AdminManagementServiceSrv.Iface.class);
var request = DamselUtil.fillRequiredTBaseObject(
new CancelParamsRequest(),
CancelParamsRequest.class

View File

@ -17,12 +17,15 @@ import dev.vality.file.storage.FileStorageSrv;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import java.util.UUID;
import static dev.vality.disputes.constant.ModerationPrefix.DISPUTES_UNKNOWN_MAPPING;
import static dev.vality.disputes.util.MockUtil.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -49,6 +52,8 @@ public class CreatedDisputesServiceTest {
private WiremockAddressesHolder wiremockAddressesHolder;
@Autowired
private CreatedDisputesTestService createdDisputesTestService;
@LocalServerPort
private int serverPort;
@Test
@SneakyThrows
@ -92,7 +97,7 @@ public class CreatedDisputesServiceTest {
@Test
@SneakyThrows
public void testManualCreatedWhenIsNotProvidersDisputesApiExist() {
public void testManualPendingWhenIsNotProvidersDisputesApiExist() {
var invoiceId = "20McecNnWoy";
var paymentId = "1";
var disputeId = UUID.fromString(disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId());
@ -105,7 +110,7 @@ public class CreatedDisputesServiceTest {
when(dominantService.getProxy(any())).thenReturn(createProxy().get());
var dispute = disputeDao.get(disputeId);
createdDisputesService.callCreateDisputeRemotely(dispute.get());
assertEquals(DisputeStatus.manual_created, disputeDao.get(disputeId).get().getStatus());
assertEquals(DisputeStatus.manual_pending, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@ -138,6 +143,83 @@ public class CreatedDisputesServiceTest {
assertEquals(DisputeStatus.failed, disputeDao.get(disputeId).get().getStatus());
}
@Test
@SneakyThrows
public void testManualCreatedWhenDisputeCreatedFailResultWithDisputesUnknownMapping() {
var invoiceId = "20McecNnWoy";
var paymentId = "1";
var disputeId = UUID.fromString(disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId());
var invoicePayment = MockUtil.createInvoicePayment(paymentId);
invoicePayment.getPayment().setStatus(InvoicePaymentStatus.captured(new InvoicePaymentCaptured()));
when(invoicingClient.getPayment(any(), any())).thenReturn(invoicePayment);
when(fileStorageClient.generateDownloadUrl(any(), any())).thenReturn(wiremockAddressesHolder.getDownloadUrl());
var terminal = createTerminal().get();
terminal.getOptions().putAll(getOptions());
when(dominantService.getTerminal(any())).thenReturn(terminal);
when(dominantService.getProvider(any())).thenReturn(createProvider().get());
when(dominantService.getProxy(any())).thenReturn(createProxy().get());
var providerMock = mock(ProviderDisputesServiceSrv.Client.class);
var disputeCreatedFailResult = createDisputeCreatedFailResult();
disputeCreatedFailResult.getFailResult().getFailure().setCode(DISPUTES_UNKNOWN_MAPPING);
when(providerMock.createDispute(any())).thenReturn(disputeCreatedFailResult);
when(providerIfaceBuilder.buildTHSpawnClient(any())).thenReturn(providerMock);
var dispute = disputeDao.get(disputeId);
createdDisputesService.callCreateDisputeRemotely(dispute.get());
assertEquals(DisputeStatus.manual_created, disputeDao.get(disputeId).get().getStatus());
assertTrue(disputeDao.get(disputeId).get().getErrorMessage().contains(DISPUTES_UNKNOWN_MAPPING));
disputeDao.update(disputeId, DisputeStatus.failed);
}
@Test
@SneakyThrows
public void testManualCreatedWhenUnexpectedResultMapping() {
var invoiceId = "20McecNnWoy";
var paymentId = "1";
var disputeId = UUID.fromString(disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId());
var invoicePayment = MockUtil.createInvoicePayment(paymentId);
invoicePayment.getPayment().setStatus(InvoicePaymentStatus.captured(new InvoicePaymentCaptured()));
when(invoicingClient.getPayment(any(), any())).thenReturn(invoicePayment);
when(fileStorageClient.generateDownloadUrl(any(), any())).thenReturn(wiremockAddressesHolder.getDownloadUrl());
var terminal = createTerminal().get();
terminal.getOptions().putAll(getOptions());
when(dominantService.getTerminal(any())).thenReturn(terminal);
when(dominantService.getProvider(any())).thenReturn(createProvider().get());
// routeUrl = "http://127.0.0.1:8023/disputes" == exist api
when(dominantService.getProxy(any())).thenReturn(createProxyWithRealAddress(serverPort).get());
var providerMock = mock(ProviderDisputesServiceSrv.Client.class);
when(providerMock.createDispute(any())).thenThrow(getUnexpectedResultWException());
when(providerIfaceBuilder.buildTHSpawnClient(any())).thenReturn(providerMock);
var dispute = disputeDao.get(disputeId);
createdDisputesService.callCreateDisputeRemotely(dispute.get());
assertEquals(DisputeStatus.manual_created, disputeDao.get(disputeId).get().getStatus());
assertTrue(disputeDao.get(disputeId).get().getErrorMessage().contains("Unexpected result"));
disputeDao.update(disputeId, DisputeStatus.failed);
}
@Test
@SneakyThrows
public void testManualPendingWhenUnexpectedResult() {
var invoiceId = "20McecNnWoy";
var paymentId = "1";
var disputeId = UUID.fromString(disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId());
var invoicePayment = MockUtil.createInvoicePayment(paymentId);
invoicePayment.getPayment().setStatus(InvoicePaymentStatus.captured(new InvoicePaymentCaptured()));
when(invoicingClient.getPayment(any(), any())).thenReturn(invoicePayment);
when(fileStorageClient.generateDownloadUrl(any(), any())).thenReturn(wiremockAddressesHolder.getDownloadUrl());
var terminal = createTerminal().get();
terminal.getOptions().putAll(getOptions());
when(dominantService.getTerminal(any())).thenReturn(terminal);
when(dominantService.getProvider(any())).thenReturn(createProvider().get());
when(dominantService.getProxy(any())).thenReturn(createProxyNotFoundCase(serverPort).get());
var providerMock = mock(ProviderDisputesServiceSrv.Client.class);
when(providerMock.createDispute(any())).thenThrow(getUnexpectedResultWException());
when(providerIfaceBuilder.buildTHSpawnClient(any())).thenReturn(providerMock);
var dispute = disputeDao.get(disputeId);
createdDisputesService.callCreateDisputeRemotely(dispute.get());
assertEquals(DisputeStatus.manual_pending, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@Test
@SneakyThrows
public void testDisputeCreatedAlreadyExistResult() {

View File

@ -15,8 +15,10 @@ import org.springframework.context.annotation.Import;
import java.util.UUID;
import static dev.vality.disputes.constant.ModerationPrefix.DISPUTES_UNKNOWN_MAPPING;
import static dev.vality.disputes.util.MockUtil.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -77,6 +79,37 @@ public class PendingDisputesServiceTest {
assertEquals(DisputeStatus.failed, disputeDao.get(disputeId).get().getStatus());
}
@Test
@SneakyThrows
public void testManualPendingWhenStatusFailResultWithDisputesUnknownMapping() {
var disputeId = createdDisputesTestService.callCreateDisputeRemotely();
var providerMock = mock(ProviderDisputesServiceSrv.Client.class);
var disputeStatusFailResult = createDisputeStatusFailResult();
disputeStatusFailResult.getStatusFail().getFailure().setCode(DISPUTES_UNKNOWN_MAPPING);
when(providerMock.checkDisputeStatus(any())).thenReturn(disputeStatusFailResult);
when(providerIfaceBuilder.buildTHSpawnClient(any())).thenReturn(providerMock);
var dispute = disputeDao.get(disputeId);
pendingDisputesService.callPendingDisputeRemotely(dispute.get());
assertEquals(DisputeStatus.manual_pending, disputeDao.get(disputeId).get().getStatus());
assertTrue(disputeDao.get(disputeId).get().getErrorMessage().contains(DISPUTES_UNKNOWN_MAPPING));
disputeDao.update(disputeId, DisputeStatus.failed);
}
@Test
@SneakyThrows
public void testManualPendingWhenUnexpectedResultMapping() {
var disputeId = createdDisputesTestService.callCreateDisputeRemotely();
var providerMock = mock(ProviderDisputesServiceSrv.Client.class);
when(providerMock.checkDisputeStatus(any())).thenThrow(getUnexpectedResultWException());
when(providerIfaceBuilder.buildTHSpawnClient(any())).thenReturn(providerMock);
var dispute = disputeDao.get(disputeId);
pendingDisputesService.callPendingDisputeRemotely(dispute.get());
assertEquals(DisputeStatus.manual_pending, disputeDao.get(disputeId).get().getStatus());
assertTrue(disputeDao.get(disputeId).get().getErrorMessage().contains("Unexpected result"));
disputeDao.update(disputeId, DisputeStatus.failed);
}
@Test
@SneakyThrows
public void testDisputeStatusPendingResult() {

View File

@ -0,0 +1,13 @@
package dev.vality.disputes.schedule.service.config;
import dev.vality.disputes.admin.AdminCallbackServiceSrv;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
@TestConfiguration
public class CallbackNotifierTestConfig {
@MockBean
private AdminCallbackServiceSrv.Iface adminCallbackDisputesTgBotClient;
}

View File

@ -27,7 +27,7 @@ import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@TestComponent
@Import({DisputeApiTestService.class, RemoteClientTestConfig.class})
@Import({DisputeApiTestService.class, RemoteClientTestConfig.class, DefaultRemoteClientTestConfig.class, CallbackNotifierTestConfig.class})
@SuppressWarnings({"ParameterName", "LineLength"})
public class CreatedDisputesTestService {

View File

@ -0,0 +1,13 @@
package dev.vality.disputes.schedule.service.config;
import dev.vality.disputes.provider.ProviderDisputesServiceSrv;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.mock.mockito.MockBean;
@TestConfiguration
public class DefaultRemoteClientTestConfig {
@MockBean
private ProviderDisputesServiceSrv.Iface providerDisputesTgBotClient;
}

View File

@ -14,6 +14,10 @@ import dev.vality.file.storage.NewFileResult;
import dev.vality.geck.common.util.TypeUtil;
import dev.vality.token.keeper.AuthData;
import dev.vality.token.keeper.AuthDataStatus;
import dev.vality.woody.api.flow.error.WErrorDefinition;
import dev.vality.woody.api.flow.error.WErrorSource;
import dev.vality.woody.api.flow.error.WErrorType;
import dev.vality.woody.api.flow.error.WRuntimeException;
import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import org.apache.thrift.TSerializer;
@ -81,8 +85,16 @@ public class MockUtil {
.setProxy(new Proxy().setRef(new ProxyRef().setId(1))));
}
public static CompletableFuture<ProxyDefinition> createProxyNotFoundCase(Integer port) {
return createProxy("http://127.0.0.1:" + port + "/debug/disputes-api/admin-management");
}
public static CompletableFuture<ProxyDefinition> createProxyWithRealAddress(Integer port) {
return createProxy("http://127.0.0.1:" + port);
}
public static CompletableFuture<ProxyDefinition> createProxy() {
return createProxy("http://ya.ru");
return createProxy("http://127.0.0.1:8023");
}
public static CompletableFuture<ProxyDefinition> createProxy(String url) {
@ -173,4 +185,13 @@ public class MockUtil {
failure.setSub(new SubFailure("some_suberror"));
return failure;
}
public static WRuntimeException getUnexpectedResultWException() {
var errorDefinition = new WErrorDefinition(WErrorSource.EXTERNAL);
errorDefinition.setErrorReason("Unexpected result, code = resp_status_error, description = " +
"Tek seferde en fazla 4,000.00 işem yapılabilir.");
errorDefinition.setErrorType(WErrorType.UNEXPECTED_ERROR);
errorDefinition.setErrorSource(WErrorSource.INTERNAL);
return new WRuntimeException(errorDefinition);
}
}