fix adjustments (#25)

* fix adjusments

* fix adjusments

* 400 when pending payment
This commit is contained in:
Anatolii Karlov 2024-10-31 19:17:02 +07:00 committed by GitHub
parent df25417b6b
commit e930555f06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
30 changed files with 253 additions and 169 deletions

View File

@ -10,7 +10,7 @@ public interface CallbackNotifier {
void sendDisputePoolingExpired(Dispute dispute);
void sendDisputeReadyForCreateAdjustment(List<Dispute> disputes);
void sendDisputesReadyForCreateAdjustment(List<Dispute> disputes);
void sendDisputeFailedReviewRequired(Dispute dispute, String errorCode, String errorDescription);

View File

@ -34,13 +34,13 @@ public class DisputesTgBotCallbackNotifierImpl implements CallbackNotifier {
}
@Override
public void sendDisputeReadyForCreateAdjustment(List<Dispute> disputes) {
public void sendDisputesReadyForCreateAdjustment(List<Dispute> disputes) {
var disputeReadyForCreateAdjustments = disputes.stream()
.map(Dispute::getId)
.map(UUID::toString)
.map(DisputeReadyForCreateAdjustment::new)
.toList();
disputesTgBotService.sendDisputeReadyForCreateAdjustment(disputeReadyForCreateAdjustments);
disputesTgBotService.sendDisputesReadyForCreateAdjustment(disputeReadyForCreateAdjustments);
}
@Override

View File

@ -26,7 +26,7 @@ public class DummyCallbackNotifierImpl implements CallbackNotifier {
}
@Override
public void sendDisputeReadyForCreateAdjustment(List<Dispute> disputes) {
public void sendDisputesReadyForCreateAdjustment(List<Dispute> disputes) {
log.debug("Trying to call DummyCallbackNotifierImpl.sendDisputeReadyForCreateAdjustment() {}", disputes.size());
}

View File

@ -1,6 +1,7 @@
package dev.vality.disputes.api.controller;
import dev.vality.disputes.exception.AuthorizationException;
import dev.vality.disputes.exception.InvoicingPaymentStatusPendingException;
import dev.vality.disputes.exception.NotFoundException;
import dev.vality.disputes.exception.TokenKeeperException;
import dev.vality.swag.disputes.model.GeneralError;
@ -35,6 +36,14 @@ public class ErrorControllerAdvice {
// ----------------- 4xx -----------------------------------------------------
@ExceptionHandler({InvoicingPaymentStatusPendingException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleInvalidMimeTypeException(InvoicingPaymentStatusPendingException e) {
log.warn("<- Res [400]: Payment has non-final status", e);
return new GeneralError()
.message("Payment has non-final status");
}
@ExceptionHandler({InvalidMimeTypeException.class})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object handleInvalidMimeTypeException(InvalidMimeTypeException e) {

View File

@ -5,4 +5,7 @@ public class InvoicingPaymentStatusPendingException extends RuntimeException {
public InvoicingPaymentStatusPendingException(Throwable cause) {
super(cause);
}
public InvoicingPaymentStatusPendingException() {
}
}

View File

@ -2,7 +2,7 @@ package dev.vality.disputes.schedule;
import dev.vality.disputes.admin.callback.CallbackNotifier;
import dev.vality.disputes.admin.management.MdcTopicProducer;
import dev.vality.disputes.schedule.service.CreateAdjustmentsService;
import dev.vality.disputes.schedule.core.AdjustmentsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -14,18 +14,18 @@ import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
public class TaskReadyForCreateAdjustmentsService {
public class AdjustmentsReadyNotificationTask {
private final CreateAdjustmentsService createAdjustmentsService;
private final AdjustmentsService adjustmentsService;
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();
var disputes = adjustmentsService.getReadyDisputesForCreateAdjustment();
mdcTopicProducer.sendReadyForCreateAdjustments(disputes);
callbackNotifier.sendDisputeReadyForCreateAdjustment(disputes);
callbackNotifier.sendDisputesReadyForCreateAdjustment(disputes);
log.info("ReadyForCreateAdjustments were processed");
}
}

View File

@ -1,8 +1,8 @@
package dev.vality.disputes.schedule;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.handler.CreateAdjustmentHandler;
import dev.vality.disputes.schedule.service.CreateAdjustmentsService;
import dev.vality.disputes.schedule.core.AdjustmentsService;
import dev.vality.disputes.schedule.handler.AdjustmentHandler;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -20,10 +20,10 @@ import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
public class TaskCreateAdjustmentsService {
public class AdjustmentsTask {
private final ExecutorService disputesThreadPool;
private final CreateAdjustmentsService createAdjustmentsService;
private final AdjustmentsService adjustmentsService;
@Value("${dispute.batchSize}")
private int batchSize;
@ -31,7 +31,7 @@ public class TaskCreateAdjustmentsService {
public void processPending() {
log.debug("Processing create adjustments get started");
try {
var disputes = createAdjustmentsService.getDisputesForHgCall(batchSize);
var disputes = adjustmentsService.getDisputesForHgCall(batchSize);
var callables = disputes.stream()
.map(this::handleCreateAdjustment)
.collect(Collectors.toList());
@ -46,6 +46,6 @@ public class TaskCreateAdjustmentsService {
}
private Callable<UUID> handleCreateAdjustment(Dispute dispute) {
return () -> new CreateAdjustmentHandler(createAdjustmentsService).handle(dispute);
return () -> new AdjustmentHandler(adjustmentsService).handle(dispute);
}
}

View File

@ -1,8 +1,8 @@
package dev.vality.disputes.schedule;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.core.CreatedDisputesService;
import dev.vality.disputes.schedule.handler.CreatedDisputeHandler;
import dev.vality.disputes.schedule.service.CreatedDisputesService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -19,7 +19,7 @@ import java.util.stream.Collectors;
@ConditionalOnProperty(value = "dispute.isScheduleCreatedEnabled", havingValue = "true")
@Service
@RequiredArgsConstructor
public class TaskCreatedDisputesService {
public class CreatedDisputesTask {
private final ExecutorService disputesThreadPool;
private final CreatedDisputesService createdDisputesService;

View File

@ -1,8 +1,8 @@
package dev.vality.disputes.schedule;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.core.PendingDisputesService;
import dev.vality.disputes.schedule.handler.PendingDisputeHandler;
import dev.vality.disputes.schedule.service.PendingDisputesService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -19,7 +19,7 @@ import java.util.stream.Collectors;
@ConditionalOnProperty(value = "dispute.isSchedulePendingEnabled", havingValue = "true")
@Service
@RequiredArgsConstructor
public class TaskPendingDisputesService {
public class PendingDisputesTask {
private final ExecutorService disputesThreadPool;
private final PendingDisputesService pendingDisputesService;

View File

@ -1,39 +1,31 @@
package dev.vality.disputes.schedule.converter;
import dev.vality.damsel.domain.*;
import dev.vality.damsel.domain.InvoicePaymentAdjustmentStatusChange;
import dev.vality.damsel.domain.InvoicePaymentCaptured;
import dev.vality.damsel.domain.InvoicePaymentStatus;
import dev.vality.damsel.payment_processing.InvoicePaymentAdjustmentParams;
import dev.vality.damsel.payment_processing.InvoicePaymentAdjustmentScenario;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.AdjustmentExtractor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class InvoicePaymentAdjustmentParamsConverter {
@RequiredArgsConstructor
public class InvoicePaymentCapturedAdjustmentParamsConverter {
public static final String DISPUTE_MASK = "disputeId=%s";
private final AdjustmentExtractor adjustmentExtractor;
public InvoicePaymentAdjustmentParams convert(Dispute dispute) {
var captured = new InvoicePaymentCaptured();
var reason = getReason(dispute);
var reason = adjustmentExtractor.getReason(dispute);
captured.setReason(reason);
var changedAmount = dispute.getChangedAmount();
if (changedAmount != null) {
var cost = new Cash(changedAmount, new CurrencyRef(dispute.getCurrencySymbolicCode()));
captured.setCost(cost);
}
var params = new InvoicePaymentAdjustmentParams();
params.setReason(reason);
params.setScenario(getInvoicePaymentAdjustmentScenario(captured));
return params;
}
public String getReason(Dispute dispute) {
return Optional.ofNullable(dispute.getReason())
.map(s -> String.format(DISPUTE_MASK + ", reason=%s", dispute.getId(), s))
.orElse(String.format(DISPUTE_MASK, dispute.getId()));
}
private InvoicePaymentAdjustmentScenario getInvoicePaymentAdjustmentScenario(InvoicePaymentCaptured captured) {
return InvoicePaymentAdjustmentScenario.status_change(new InvoicePaymentAdjustmentStatusChange(
InvoicePaymentStatus.captured(captured)));

View File

@ -0,0 +1,32 @@
package dev.vality.disputes.schedule.converter;
import dev.vality.damsel.domain.InvoicePaymentAdjustmentCashFlow;
import dev.vality.damsel.domain.InvoicePaymentCaptured;
import dev.vality.damsel.payment_processing.InvoicePaymentAdjustmentParams;
import dev.vality.damsel.payment_processing.InvoicePaymentAdjustmentScenario;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.AdjustmentExtractor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class InvoicePaymentCashFlowAdjustmentParamsConverter {
private final AdjustmentExtractor adjustmentExtractor;
public InvoicePaymentAdjustmentParams convert(Dispute dispute) {
var captured = new InvoicePaymentCaptured();
var reason = adjustmentExtractor.getReason(dispute);
captured.setReason(reason);
var params = new InvoicePaymentAdjustmentParams();
params.setReason(reason);
params.setScenario(getInvoicePaymentAdjustmentScenario(dispute.getChangedAmount()));
return params;
}
private InvoicePaymentAdjustmentScenario getInvoicePaymentAdjustmentScenario(Long changedAmount) {
return InvoicePaymentAdjustmentScenario.cash_flow(new InvoicePaymentAdjustmentCashFlow()
.setNewAmount(changedAmount));
}
}

View File

@ -0,0 +1,32 @@
package dev.vality.disputes.schedule.converter;
import dev.vality.damsel.domain.*;
import dev.vality.damsel.payment_processing.InvoicePaymentAdjustmentParams;
import dev.vality.damsel.payment_processing.InvoicePaymentAdjustmentScenario;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.AdjustmentExtractor;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class InvoicePaymentFailedAdjustmentParamsConverter {
private final AdjustmentExtractor adjustmentExtractor;
public InvoicePaymentAdjustmentParams convert(Dispute dispute) {
var invoicePaymentFailed = new InvoicePaymentFailed();
var reason = adjustmentExtractor.getReason(dispute);
invoicePaymentFailed.setFailure(OperationFailure.failure(
new Failure("failed_by_disputes_api").setReason(reason)));
var params = new InvoicePaymentAdjustmentParams();
params.setReason(reason);
params.setScenario(getInvoicePaymentAdjustmentScenario(invoicePaymentFailed));
return params;
}
private InvoicePaymentAdjustmentScenario getInvoicePaymentAdjustmentScenario(InvoicePaymentFailed failed) {
return InvoicePaymentAdjustmentScenario.status_change(new InvoicePaymentAdjustmentStatusChange(
InvoicePaymentStatus.failed(failed)));
}
}

View File

@ -1,4 +1,4 @@
package dev.vality.disputes.schedule.service;
package dev.vality.disputes.schedule.core;
import dev.vality.damsel.domain.InvoicePaymentAdjustment;
import dev.vality.damsel.payment_processing.InvoicePayment;
@ -7,8 +7,10 @@ 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.exception.InvoicingPaymentStatusPendingException;
import dev.vality.disputes.schedule.converter.InvoicePaymentAdjustmentParamsConverter;
import dev.vality.disputes.schedule.converter.InvoicePaymentCapturedAdjustmentParamsConverter;
import dev.vality.disputes.schedule.converter.InvoicePaymentCashFlowAdjustmentParamsConverter;
import dev.vality.disputes.schedule.converter.InvoicePaymentFailedAdjustmentParamsConverter;
import dev.vality.disputes.schedule.service.AdjustmentExtractor;
import dev.vality.disputes.service.external.InvoicingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -18,16 +20,19 @@ import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
@Slf4j
@Service
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength", "MissingSwitchDefault"})
public class CreateAdjustmentsService {
public class AdjustmentsService {
private final DisputeDao disputeDao;
private final InvoicingService invoicingService;
private final InvoicePaymentAdjustmentParamsConverter invoicePaymentAdjustmentParamsConverter;
private final InvoicePaymentCapturedAdjustmentParamsConverter invoicePaymentCapturedAdjustmentParamsConverter;
private final InvoicePaymentCashFlowAdjustmentParamsConverter invoicePaymentCashFlowAdjustmentParamsConverter;
private final InvoicePaymentFailedAdjustmentParamsConverter invoicePaymentFailedAdjustmentParamsConverter;
private final AdjustmentExtractor adjustmentExtractor;
@Transactional(propagation = Propagation.REQUIRED)
@ -57,42 +62,51 @@ 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.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, ErrorReason.PAYMENT_NOT_FOUND);
log.debug("Dispute status has been set to failed {}", dispute.getId());
updateFailed(dispute, ErrorReason.PAYMENT_NOT_FOUND);
return;
}
var invoicePaymentAdjustment = adjustmentExtractor.searchAdjustmentByDispute(invoicePayment, dispute);
if (invoicePaymentAdjustment.isPresent()) {
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.getId());
return;
}
try {
var params = invoicePaymentAdjustmentParamsConverter.convert(dispute);
if (!adjustmentExtractor.isCashFlowAdjustmentByDisputeExist(invoicePayment, dispute)
&& !Objects.equals(dispute.getAmount(), dispute.getChangedAmount())) {
var params = invoicePaymentCashFlowAdjustmentParamsConverter.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.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, ErrorReason.INVOICE_NOT_FOUND);
log.debug("Dispute status has been set to failed {}", dispute.getId());
var errorReason = ErrorReason.INVOICE_NOT_FOUND;
updateFailed(dispute, errorReason);
return;
}
} catch (InvoicingPaymentStatusPendingException e) {
// в теории 0%, что сюда попадает выполнение кода, но если попадет, то:
// платеж с не финальным статусом будет заблочен для создания корректировок на стороне хелгейта
// и тогда диспут будет пулиться, пока платеж не зафиналится,
// и тк никакой записи в коде выше нет, то пуллинг не проблема
// а запрос в checkDisputeStatus по идемпотентности просто вернет тот же success
log.error("Error when hg.createPaymentAdjustment() got payments status pending {}", dispute.getId(), e);
return;
} else {
log.info("Creating CashFlowAdjustment was skipped {}", dispute);
}
if (!adjustmentExtractor.isCapturedAdjustmentByDisputeExist(invoicePayment, dispute)) {
if (invoicePayment.getPayment().getStatus().isSetCaptured()) {
var params = invoicePaymentFailedAdjustmentParamsConverter.convert(dispute);
var paymentAdjustment = createAdjustment(dispute, params);
if (paymentAdjustment == null) {
updateFailed(dispute, ErrorReason.INVOICE_NOT_FOUND);
return;
}
}
var params = invoicePaymentCapturedAdjustmentParamsConverter.convert(dispute);
var paymentAdjustment = createAdjustment(dispute, params);
if (paymentAdjustment == null) {
updateFailed(dispute, ErrorReason.INVOICE_NOT_FOUND);
return;
}
} else {
log.info("Creating CapturedAdjustment was skipped {}", dispute);
}
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.getId());
}
@Transactional(propagation = Propagation.REQUIRED)
void updateFailed(Dispute dispute, String errorReason) {
log.error("Trying to set failed Dispute status with {} error reason {}", errorReason, dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, errorReason);
log.debug("Dispute status has been set to failed {}", dispute.getId());
}
private InvoicePaymentAdjustment createAdjustment(Dispute dispute, InvoicePaymentAdjustmentParams params) {
return invoicingService.createPaymentAdjustment(dispute.getInvoiceId(), dispute.getPaymentId(), params);
}

View File

@ -1,4 +1,4 @@
package dev.vality.disputes.schedule.service;
package dev.vality.disputes.schedule.core;
import dev.vality.damsel.payment_processing.InvoicePayment;
import dev.vality.disputes.constant.ErrorReason;
@ -11,6 +11,8 @@ 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.schedule.service.AttachmentsService;
import dev.vality.disputes.schedule.service.ProviderDataService;
import dev.vality.disputes.service.external.InvoicingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -33,7 +35,7 @@ public class CreatedDisputesService {
private final RemoteClient remoteClient;
private final DisputeDao disputeDao;
private final CreatedAttachmentsService createdAttachmentsService;
private final AttachmentsService attachmentsService;
private final InvoicingService invoicingService;
private final ProviderDataService providerDataService;
private final DefaultRemoteClient defaultRemoteClient;
@ -64,13 +66,7 @@ public class CreatedDisputesService {
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.getId());
return;
}
var attachments = createdAttachmentsService.getAttachments(dispute);
var attachments = attachmentsService.getAttachments(dispute);
if (attachments == null || attachments.isEmpty()) {
log.error("Trying to set failed Dispute status with NO_ATTACHMENTS error reason {}", dispute.getId());
disputeDao.update(dispute.getId(), DisputeStatus.failed, ErrorReason.NO_ATTACHMENTS);
@ -79,7 +75,7 @@ public class CreatedDisputesService {
}
var providerData = providerDataService.getProviderData(dispute.getProviderId(), dispute.getTerminalId());
var options = providerData.getOptions();
if ((status.isSetCaptured() && isCapturedBlockedForDispute(options))
if ((invoicePayment.getPayment().getStatus().isSetCaptured() && isCapturedBlockedForDispute(options))
|| isNotProvidersDisputesApiExist(options)) {
// отправлять на ручной разбор, если выставлена опция
// DISPUTE_FLOW_CAPTURED_BLOCKED или не выставлена DISPUTE_FLOW_PROVIDERS_API_EXIST

View File

@ -1,4 +1,4 @@
package dev.vality.disputes.schedule.service;
package dev.vality.disputes.schedule.core;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.dao.ProviderDisputeDao;
@ -10,6 +10,7 @@ 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 dev.vality.disputes.schedule.service.ProviderDataService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

View File

@ -1,7 +1,7 @@
package dev.vality.disputes.schedule.handler;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.CreateAdjustmentsService;
import dev.vality.disputes.schedule.core.AdjustmentsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -9,16 +9,16 @@ import java.util.UUID;
@RequiredArgsConstructor
@Slf4j
public class CreateAdjustmentHandler {
public class AdjustmentHandler {
private final CreateAdjustmentsService createAdjustmentsService;
private final AdjustmentsService adjustmentsService;
public UUID handle(Dispute dispute) {
final var currentThread = Thread.currentThread();
final var oldName = currentThread.getName();
currentThread.setName("dispute-created-adjustment-id-" + dispute.getId() + "-" + oldName);
try {
createAdjustmentsService.callHgForCreateAdjustment(dispute);
adjustmentsService.callHgForCreateAdjustment(dispute);
return dispute.getId();
} catch (Throwable ex) {
log.error("Received exception while scheduler processed callHgForCreateAdjustment", ex);

View File

@ -1,7 +1,7 @@
package dev.vality.disputes.schedule.handler;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.CreatedDisputesService;
import dev.vality.disputes.schedule.core.CreatedDisputesService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@ -58,7 +58,7 @@ public class DisputeStatusResultHandler {
@Transactional(propagation = Propagation.REQUIRED)
public void handleStatusSuccess(Dispute dispute, DisputeStatusResult result) {
callbackNotifier.sendDisputeReadyForCreateAdjustment(List.of(dispute));
callbackNotifier.sendDisputesReadyForCreateAdjustment(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);

View File

@ -1,7 +1,7 @@
package dev.vality.disputes.schedule.handler;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import dev.vality.disputes.schedule.service.PendingDisputesService;
import dev.vality.disputes.schedule.core.PendingDisputesService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

View File

@ -1,11 +1,10 @@
package dev.vality.disputes.schedule.service;
import dev.vality.damsel.domain.Cash;
import dev.vality.damsel.domain.InvoicePaymentAdjustment;
import dev.vality.damsel.domain.InvoicePaymentStatus;
import dev.vality.damsel.payment_processing.InvoicePayment;
import dev.vality.disputes.domain.tables.pojos.Dispute;
import jakarta.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
@ -13,36 +12,37 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static dev.vality.disputes.schedule.converter.InvoicePaymentAdjustmentParamsConverter.DISPUTE_MASK;
@Component
@RequiredArgsConstructor
@SuppressWarnings({"ParameterName", "LineLength"})
public class AdjustmentExtractor {
public Optional<InvoicePaymentAdjustment> searchAdjustmentByDispute(InvoicePayment invoicePayment, Dispute dispute) {
return getInvoicePaymentAdjustmentStream(invoicePayment)
.filter(adj -> adj.getReason() != null)
.filter(adj -> isDisputesAdjustment(adj.getReason(), dispute))
.findFirst()
.or(() -> getInvoicePaymentAdjustmentStream(invoicePayment)
.filter(s -> s.getState() != null)
.filter(s -> s.getState().isSetStatusChange())
.filter(s -> getTargetStatus(s).isSetCaptured())
.filter(s -> getTargetStatus(s).getCaptured().getReason() != null)
.filter(s -> isDisputesAdjustment(getTargetStatus(s).getCaptured().getReason(), dispute))
.findFirst());
public static final String DISPUTE_MASK = "disputeId=%s";
public String getReason(Dispute dispute) {
return Optional.ofNullable(dispute.getReason())
.map(s -> String.format(DISPUTE_MASK + ", reason=%s", dispute.getId(), s))
.orElse(String.format(DISPUTE_MASK, dispute.getId()));
}
public Long getChangedAmount(@Nonnull InvoicePaymentAdjustment invoicePaymentAdjustment, Long changedAmount) {
return Optional.of(invoicePaymentAdjustment)
.map(s -> getTargetStatus(s).getCaptured().getCost())
.map(Cash::getAmount)
.or(() -> Optional.ofNullable(changedAmount))
.orElse(null);
public boolean isCashFlowAdjustmentByDisputeExist(InvoicePayment invoicePayment, Dispute dispute) {
return getInvoicePaymentAdjustmentStream(invoicePayment)
.filter(adj -> isDisputesAdjustment(adj.getReason(), dispute))
.anyMatch(adj -> adj.getState() != null && adj.getState().isSetCashFlow());
}
public boolean isCapturedAdjustmentByDisputeExist(InvoicePayment invoicePayment, Dispute dispute) {
return getInvoicePaymentAdjustmentStream(invoicePayment)
.filter(adj -> isDisputesAdjustment(adj.getReason(), dispute))
.filter(adj -> adj.getState() != null && adj.getState().isSetStatusChange())
.filter(adj -> getTargetStatus(adj).isSetCaptured())
.anyMatch(adj -> isDisputesAdjustment(getTargetStatus(adj).getCaptured().getReason(), dispute));
}
private Stream<InvoicePaymentAdjustment> getInvoicePaymentAdjustmentStream(InvoicePayment invoicePayment) {
return Optional.ofNullable(invoicePayment.getAdjustments()).orElse(List.of()).stream();
return Optional.ofNullable(invoicePayment.getAdjustments())
.orElse(List.of())
.stream();
}
private InvoicePaymentStatus getTargetStatus(InvoicePaymentAdjustment s) {
@ -50,6 +50,7 @@ public class AdjustmentExtractor {
}
private boolean isDisputesAdjustment(String reason, Dispute dispute) {
return !StringUtils.isBlank(reason) && reason.contains(String.format(DISPUTE_MASK, dispute.getId()));
return !StringUtils.isBlank(reason)
&& reason.equalsIgnoreCase(getReason(dispute));
}
}

View File

@ -16,7 +16,7 @@ import java.util.List;
@Service
@RequiredArgsConstructor
@Slf4j
public class CreatedAttachmentsService {
public class AttachmentsService {
private final FileMetaDao fileMetaDao;
private final FileStorageService fileStorageService;

View File

@ -3,6 +3,7 @@ package dev.vality.disputes.security;
import dev.vality.damsel.payment_processing.InvoicePayment;
import dev.vality.disputes.exception.AuthorizationException;
import dev.vality.disputes.exception.BouncerException;
import dev.vality.disputes.exception.InvoicingPaymentStatusPendingException;
import dev.vality.disputes.exception.NotFoundException;
import dev.vality.disputes.security.service.BouncerService;
import dev.vality.disputes.security.service.TokenKeeperService;
@ -93,12 +94,17 @@ public class AccessService {
private InvoicePayment getInvoicePayment(dev.vality.damsel.payment_processing.Invoice invoice, String paymentId) {
log.debug("Processing invoice: {}", invoice.getInvoice().getId());
return invoice.getPayments().stream()
.filter(invoicePayment -> paymentId.equals(invoicePayment.getPayment().getId())
&& invoicePayment.isSetRoute())
var invoicePayment = invoice.getPayments().stream()
.filter(p -> paymentId.equals(p.getPayment().getId()) && p.isSetRoute())
.findFirst()
// http 404
.orElseThrow(() -> new NotFoundException(
String.format("Payment with id: %s and filled route not found!", paymentId)));
String.format("Payment with id: %s and filled route and status not found!", paymentId)));
log.debug("Processing payment: {}", invoicePayment);
var status = invoicePayment.getPayment().getStatus();
if (!status.isSetCaptured() && !status.isSetCancelled() && !status.isSetFailed()) {
throw new InvoicingPaymentStatusPendingException();
}
return invoicePayment;
}
}

View File

@ -17,7 +17,7 @@ public interface DisputesTgBotService {
void sendDisputePoolingExpired(DisputePoolingExpired disputePoolingExpired);
void sendDisputeReadyForCreateAdjustment(List<DisputeReadyForCreateAdjustment> disputeReadyForCreateAdjustments);
void sendDisputesReadyForCreateAdjustment(List<DisputeReadyForCreateAdjustment> disputeReadyForCreateAdjustments);
void sendDisputeFailedReviewRequired(DisputeFailedReviewRequired disputeFailedReviewRequired);

View File

@ -51,13 +51,13 @@ public class DisputesTgBotServiceImpl implements DisputesTgBotService {
@Override
@SneakyThrows
public void sendDisputeReadyForCreateAdjustment(List<DisputeReadyForCreateAdjustment> disputeReadyForCreateAdjustments) {
var ids = disputeReadyForCreateAdjustments.stream()
public void sendDisputesReadyForCreateAdjustment(List<DisputeReadyForCreateAdjustment> disputesReadyForCreateAdjustments) {
var ids = disputesReadyForCreateAdjustments.stream()
.map(DisputeReadyForCreateAdjustment::getId)
.map(String::valueOf)
.collect(Collectors.joining(", "));
log.debug("Trying to call adminCallbackDisputesTgBotClient.sendDisputeReadyForCreateAdjustment() {}", ids);
var notifications = disputeReadyForCreateAdjustments.stream()
var notifications = disputesReadyForCreateAdjustments.stream()
.map(Notification::disputeReadyForCreateAdjustment)
.collect(Collectors.toList());
adminCallbackDisputesTgBotClient.notify(new NotificationParamsRequest(notifications));

View File

@ -7,7 +7,8 @@ import dev.vality.disputes.config.WireMockSpringBootITest;
import dev.vality.disputes.constant.ErrorReason;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.schedule.converter.InvoicePaymentAdjustmentParamsConverter;
import dev.vality.disputes.schedule.converter.InvoicePaymentCapturedAdjustmentParamsConverter;
import dev.vality.disputes.schedule.core.AdjustmentsService;
import dev.vality.disputes.schedule.service.config.DisputeApiTestService;
import dev.vality.disputes.schedule.service.config.PendingDisputesTestService;
import dev.vality.disputes.util.MockUtil;
@ -19,7 +20,8 @@ import org.springframework.context.annotation.Import;
import java.util.List;
import java.util.UUID;
import static dev.vality.disputes.util.MockUtil.getInvoicePaymentAdjustment;
import static dev.vality.disputes.util.MockUtil.getCapturedInvoicePaymentAdjustment;
import static dev.vality.disputes.util.MockUtil.getCashFlowInvoicePaymentAdjustment;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@ -27,20 +29,22 @@ import static org.mockito.Mockito.when;
@WireMockSpringBootITest
@Import({PendingDisputesTestService.class})
@SuppressWarnings({"ParameterName", "LineLength"})
public class CreateAdjustmentsServiceTest {
public class AdjustmentsServiceTest {
@Autowired
private InvoicingSrv.Iface invoicingClient;
@Autowired
private DisputeDao disputeDao;
@Autowired
private CreateAdjustmentsService createAdjustmentsService;
private AdjustmentsService adjustmentsService;
@Autowired
private DisputeApiTestService disputeApiTestService;
@Autowired
private PendingDisputesTestService pendingDisputesTestService;
@Autowired
private InvoicePaymentAdjustmentParamsConverter invoicePaymentAdjustmentParamsConverter;
private InvoicePaymentCapturedAdjustmentParamsConverter invoicePaymentCapturedAdjustmentParamsConverter;
@Autowired
private AdjustmentExtractor adjustmentExtractor;
@Test
@SneakyThrows
@ -50,40 +54,17 @@ public class CreateAdjustmentsServiceTest {
var disputeId = UUID.fromString(disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId());
disputeDao.update(disputeId, DisputeStatus.create_adjustment);
var dispute = disputeDao.get(disputeId);
createAdjustmentsService.callHgForCreateAdjustment(dispute.get());
adjustmentsService.callHgForCreateAdjustment(dispute.get());
assertEquals(DisputeStatus.failed, disputeDao.get(disputeId).get().getStatus());
assertEquals(ErrorReason.PAYMENT_NOT_FOUND, disputeDao.get(disputeId).get().getErrorMessage());
}
@Test
@SneakyThrows
public void testDisputesAdjustmentExist() {
var invoiceId = "20McecNnWoy";
var paymentId = "1";
var disputeId = UUID.fromString(disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId());
disputeDao.update(disputeId, DisputeStatus.create_adjustment);
var invoicePayment = MockUtil.createInvoicePayment(paymentId);
invoicePayment.getPayment().setStatus(InvoicePaymentStatus.captured(new InvoicePaymentCaptured()));
var dispute = disputeDao.get(disputeId);
dispute.get().setReason("test adj");
var adjustmentId = "adjustmentId";
var invoicePaymentAdjustment = getInvoicePaymentAdjustment(adjustmentId, invoicePaymentAdjustmentParamsConverter.getReason(dispute.get()));
invoicePayment.setAdjustments(List.of(invoicePaymentAdjustment));
when(invoicingClient.getPayment(any(), any())).thenReturn(invoicePayment);
createAdjustmentsService.callHgForCreateAdjustment(dispute.get());
assertEquals(DisputeStatus.succeeded, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@Test
@SneakyThrows
public void testInvoiceNotFound() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
var paymentId = "1";
var invoicePayment = MockUtil.createInvoicePayment(paymentId);
invoicePayment.getPayment().setStatus(InvoicePaymentStatus.captured(new InvoicePaymentCaptured()));
var dispute = disputeDao.get(disputeId);
createAdjustmentsService.callHgForCreateAdjustment(dispute.get());
adjustmentsService.callHgForCreateAdjustment(dispute.get());
assertEquals(DisputeStatus.failed, disputeDao.get(disputeId).get().getStatus());
assertEquals(ErrorReason.INVOICE_NOT_FOUND, disputeDao.get(disputeId).get().getErrorMessage());
}
@ -92,16 +73,33 @@ public class CreateAdjustmentsServiceTest {
@SneakyThrows
public void testFullSuccessFlow() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
var paymentId = "1";
var invoicePayment = MockUtil.createInvoicePayment(paymentId);
invoicePayment.getPayment().setStatus(InvoicePaymentStatus.captured(new InvoicePaymentCaptured()));
var dispute = disputeDao.get(disputeId);
dispute.get().setReason("test adj");
dispute.get().setChangedAmount(dispute.get().getAmount() + 1);
var adjustmentId = "adjustmentId";
var reason = invoicePaymentAdjustmentParamsConverter.getReason(dispute.get());
var reason = adjustmentExtractor.getReason(dispute.get());
when(invoicingClient.createPaymentAdjustment(any(), any(), any()))
.thenReturn(getInvoicePaymentAdjustment(adjustmentId, reason));
createAdjustmentsService.callHgForCreateAdjustment(dispute.get());
.thenReturn(getCapturedInvoicePaymentAdjustment(adjustmentId, reason));
adjustmentsService.callHgForCreateAdjustment(dispute.get());
assertEquals(DisputeStatus.succeeded, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@Test
@SneakyThrows
public void testDisputesAdjustmentExist() {
var disputeId = pendingDisputesTestService.callPendingDisputeRemotely();
var dispute = disputeDao.get(disputeId);
dispute.get().setReason("test adj");
dispute.get().setChangedAmount(dispute.get().getAmount() + 1);
var adjustmentId = "adjustmentId";
var invoicePayment = MockUtil.createInvoicePayment(dispute.get().getPaymentId());
invoicePayment.getPayment().setStatus(InvoicePaymentStatus.captured(new InvoicePaymentCaptured()));
invoicePayment.setAdjustments(List.of(
getCapturedInvoicePaymentAdjustment(adjustmentId, adjustmentExtractor.getReason(dispute.get())),
getCashFlowInvoicePaymentAdjustment(adjustmentId, adjustmentExtractor.getReason(dispute.get()))));
when(invoicingClient.getPayment(any(), any())).thenReturn(invoicePayment);
adjustmentsService.callHgForCreateAdjustment(dispute.get());
assertEquals(DisputeStatus.succeeded, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}

View File

@ -8,6 +8,7 @@ import dev.vality.disputes.constant.ErrorReason;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.provider.ProviderDisputesServiceSrv;
import dev.vality.disputes.schedule.core.CreatedDisputesService;
import dev.vality.disputes.schedule.service.config.CreatedDisputesTestService;
import dev.vality.disputes.schedule.service.config.DisputeApiTestService;
import dev.vality.disputes.schedule.service.config.WiremockAddressesHolder;
@ -67,19 +68,6 @@ public class CreatedDisputesServiceTest {
assertEquals(ErrorReason.PAYMENT_NOT_FOUND, disputeDao.get(disputeId).get().getErrorMessage());
}
@Test
@SneakyThrows
public void testSkipDisputeWhenPaymentNonFinalStatus() {
var invoiceId = "20McecNnWoy";
var paymentId = "1";
var disputeId = UUID.fromString(disputeApiTestService.createDisputeViaApi(invoiceId, paymentId).getDisputeId());
when(invoicingClient.getPayment(any(), any())).thenReturn(MockUtil.createInvoicePayment(paymentId));
var dispute = disputeDao.get(disputeId);
createdDisputesService.callCreateDisputeRemotely(dispute.get());
assertEquals(DisputeStatus.created, disputeDao.get(disputeId).get().getStatus());
disputeDao.update(disputeId, DisputeStatus.failed);
}
@Test
@SneakyThrows
public void testNoAttachments() {

View File

@ -4,6 +4,7 @@ import dev.vality.disputes.config.WireMockSpringBootITest;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.provider.ProviderDisputesServiceSrv;
import dev.vality.disputes.schedule.core.PendingDisputesService;
import dev.vality.disputes.schedule.service.config.CreatedDisputesTestService;
import dev.vality.disputes.schedule.service.config.DisputeApiTestService;
import dev.vality.disputes.schedule.service.config.PendingDisputesTestService;

View File

@ -6,7 +6,7 @@ import dev.vality.damsel.payment_processing.InvoicingSrv;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.provider.ProviderDisputesServiceSrv;
import dev.vality.disputes.schedule.service.CreatedDisputesService;
import dev.vality.disputes.schedule.core.CreatedDisputesService;
import dev.vality.disputes.schedule.service.ProviderIfaceBuilder;
import dev.vality.disputes.service.external.DominantService;
import dev.vality.disputes.util.MockUtil;

View File

@ -3,7 +3,7 @@ package dev.vality.disputes.schedule.service.config;
import dev.vality.disputes.dao.DisputeDao;
import dev.vality.disputes.domain.enums.DisputeStatus;
import dev.vality.disputes.provider.ProviderDisputesServiceSrv;
import dev.vality.disputes.schedule.service.PendingDisputesService;
import dev.vality.disputes.schedule.core.PendingDisputesService;
import dev.vality.disputes.schedule.service.ProviderIfaceBuilder;
import lombok.SneakyThrows;
import org.springframework.beans.factory.annotation.Autowired;

View File

@ -58,7 +58,9 @@ public class MockUtil {
BankCard.class))))))
.setCost(new Cash()
.setCurrency(new CurrencyRef().setSymbolicCode("RUB")))
.setStatus(InvoicePaymentStatus.pending(new InvoicePaymentPending())))
.setStatus(InvoicePaymentStatus.failed(
new InvoicePaymentFailed(OperationFailure.failure(
new Failure("authorization_failed:unknown"))))))
.setRoute(new PaymentRoute()
.setProvider(DamselUtil.fillRequiredTBaseObject(new ProviderRef(), ProviderRef.class))
.setTerminal(DamselUtil.fillRequiredTBaseObject(new TerminalRef(), TerminalRef.class)))
@ -170,9 +172,10 @@ public class MockUtil {
return DisputeStatusResult.statusPending(new DisputeStatusPendingResult());
}
public static InvoicePaymentAdjustment getInvoicePaymentAdjustment(String adjustmentId, String reason) {
public static InvoicePaymentAdjustment getCapturedInvoicePaymentAdjustment(String adjustmentId, String reason) {
return new InvoicePaymentAdjustment()
.setId(adjustmentId)
.setReason(reason)
.setState(InvoicePaymentAdjustmentState.status_change(new InvoicePaymentAdjustmentStatusChangeState()
.setScenario(new InvoicePaymentAdjustmentStatusChange()
.setTargetStatus(new InvoicePaymentStatus(InvoicePaymentStatus.captured(
@ -180,6 +183,14 @@ public class MockUtil {
.setReason(reason)))))));
}
public static InvoicePaymentAdjustment getCashFlowInvoicePaymentAdjustment(String adjustmentId, String reason) {
return new InvoicePaymentAdjustment()
.setId(adjustmentId)
.setReason(reason)
.setState(InvoicePaymentAdjustmentState.cash_flow(new InvoicePaymentAdjustmentCashFlowState()
.setScenario(new InvoicePaymentAdjustmentCashFlow().setNewAmount(10L))));
}
public static Failure createFailure() {
Failure failure = new Failure("some_error");
failure.setSub(new SubFailure("some_suberror"));