From 8a223291656bdcb5450a82570c35d91a53688590 Mon Sep 17 00:00:00 2001 From: Egor Cherniak Date: Fri, 31 Jul 2026 15:19:29 +0300 Subject: [PATCH] Set actual cache on status success change --- pom.xml | 7 ++- ...awalStatusChangedHookMessageGenerator.java | 57 ++++++++++++++----- .../hooker/handler/TestBeanFactory.java | 55 +++++++++++++++++- .../handler/WithdrawalEventHandlerTest.java | 32 ++++++++++- 4 files changed, 131 insertions(+), 20 deletions(-) diff --git a/pom.xml b/pom.xml index 7b5d431..85e3b56 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ dev.vality service-parent-pom - 3.1.9 + 3.1.11 wallets-hooker @@ -124,16 +124,17 @@ dev.vality damsel + 1.698-99c91c9 dev.vality fistful-proto - 1.180-8b04c7f + 1.191-94b75ce dev.vality swag-wallets-webhook-events-server - 1.42-d30b8f0 + 1.43-1b943c0 com.google.guava diff --git a/src/main/java/dev/vality/wallets/hooker/handler/withdrawal/generator/WithdrawalStatusChangedHookMessageGenerator.java b/src/main/java/dev/vality/wallets/hooker/handler/withdrawal/generator/WithdrawalStatusChangedHookMessageGenerator.java index 987ded3..c3d1a82 100644 --- a/src/main/java/dev/vality/wallets/hooker/handler/withdrawal/generator/WithdrawalStatusChangedHookMessageGenerator.java +++ b/src/main/java/dev/vality/wallets/hooker/handler/withdrawal/generator/WithdrawalStatusChangedHookMessageGenerator.java @@ -2,10 +2,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import dev.vality.fistful.base.Cash; +import dev.vality.fistful.withdrawal.WithdrawalState; import dev.vality.fistful.withdrawal.StatusChange; import dev.vality.fistful.withdrawal.status.Status; import dev.vality.swag.wallets.webhook.events.model.Event; import dev.vality.swag.wallets.webhook.events.model.Fee; +import dev.vality.swag.wallets.webhook.events.model.WithdrawalBody; import dev.vality.swag.wallets.webhook.events.model.WithdrawalFailed; import dev.vality.swag.wallets.webhook.events.model.WithdrawalSucceeded; import dev.vality.wallets.hooker.domain.WebHookModel; @@ -103,11 +106,13 @@ private String initRequestBody( withdrawalFailed.setTopic(Event.TopicEnum.WITHDRAWAL_TOPIC); return objectMapper.writeValueAsString(withdrawalFailed); } else if (status.isSetSucceeded()) { - Fee fee = calculateFee(withdrawalId, eventId); + WithdrawalState withdrawalState = getWithdrawalState(withdrawalId, eventId); + Fee fee = calculateFee(withdrawalId, eventId, withdrawalState); WithdrawalSucceeded withdrawalSucceeded = new WithdrawalSucceeded() .withdrawalID(withdrawalId) .externalID(externalId) - .fee(fee); + .fee(fee) + .body(initNewBody(withdrawalState)); withdrawalSucceeded.setEventType(Event.EventTypeEnum.WITHDRAWAL_SUCCEEDED); withdrawalSucceeded.setEventID(eventId.toString()); withdrawalSucceeded.setOccuredAt(OffsetDateTime.parse(createdAt)); @@ -122,24 +127,46 @@ private String initRequestBody( } } - private Fee calculateFee(String withdrawalId, Long eventId) { + private WithdrawalState getWithdrawalState(String withdrawalId, Long eventId) { try { - var withdrawalState = withdrawalClient.getWithdrawalInfo(withdrawalId, eventId); - if (withdrawalState != null - && withdrawalState.getEffectiveFinalCashFlow() != null - && withdrawalState.getEffectiveFinalCashFlow().getPostings() != null) { - long amount = CashFlowUtils.getWithdrawalFee(withdrawalState.getEffectiveFinalCashFlow().getPostings()); - String currency = withdrawalState.getBody().getCurrency().getSymbolicCode(); - return new Fee().amount(amount).currency(currency); - } - log.warn("Unable to calculate fee for withdrawalId={}, eventId={}: missing cash flow data", - withdrawalId, eventId); - return null; + return withdrawalClient.getWithdrawalInfo(withdrawalId, eventId); } catch (Exception e) { - log.warn("Error calculating fee for withdrawalId={}, eventId={}: {}", + log.warn("Error getting withdrawal state for withdrawalId={}, eventId={}: {}", withdrawalId, eventId, e.getMessage()); return null; } } + private Fee calculateFee(String withdrawalId, Long eventId, WithdrawalState withdrawalState) { + if (withdrawalState != null + && withdrawalState.getEffectiveFinalCashFlow() != null + && withdrawalState.getEffectiveFinalCashFlow().getPostings() != null) { + long amount = CashFlowUtils.getWithdrawalFee(withdrawalState.getEffectiveFinalCashFlow().getPostings()); + String currency = withdrawalState.getBody().getCurrency().getSymbolicCode(); + return new Fee().amount(amount).currency(currency); + } + log.warn("Unable to calculate fee for withdrawalId={}, eventId={}: missing cash flow data", + withdrawalId, eventId); + return null; + } + + private WithdrawalBody initNewBody(WithdrawalState withdrawalState) { + if (!amountChanged(withdrawalState)) { + return null; + } + return initBody(withdrawalState.getNewBody()); + } + + private boolean amountChanged(WithdrawalState withdrawalState) { + return withdrawalState != null + && withdrawalState.isSetNewBody(); + } + + private WithdrawalBody initBody(Cash body) { + var withdrawalBody = new WithdrawalBody(); + withdrawalBody.setAmount(body.getAmount()); + withdrawalBody.setCurrency(body.getCurrency().getSymbolicCode()); + return withdrawalBody; + } + } diff --git a/src/test/java/dev/vality/wallets/hooker/handler/TestBeanFactory.java b/src/test/java/dev/vality/wallets/hooker/handler/TestBeanFactory.java index 33a175e..816455c 100644 --- a/src/test/java/dev/vality/wallets/hooker/handler/TestBeanFactory.java +++ b/src/test/java/dev/vality/wallets/hooker/handler/TestBeanFactory.java @@ -2,11 +2,18 @@ import dev.vality.fistful.account.Account; import dev.vality.fistful.base.*; +import dev.vality.fistful.cashflow.CashFlowAccount; +import dev.vality.fistful.cashflow.FinalCashFlow; +import dev.vality.fistful.cashflow.FinalCashFlowAccount; +import dev.vality.fistful.cashflow.FinalCashFlowPosting; +import dev.vality.fistful.cashflow.SystemCashFlowAccount; +import dev.vality.fistful.cashflow.WalletCashFlowAccount; import dev.vality.fistful.destination.Destination; import dev.vality.fistful.destination.TimestampedChange; import dev.vality.fistful.withdrawal.CreatedChange; import dev.vality.fistful.withdrawal.StatusChange; import dev.vality.fistful.withdrawal.Withdrawal; +import dev.vality.fistful.withdrawal.WithdrawalState; import dev.vality.fistful.withdrawal.status.Status; import dev.vality.fistful.withdrawal.status.Succeeded; import dev.vality.kafka.common.serialization.ThriftSerializer; @@ -17,6 +24,7 @@ import org.apache.thrift.TBase; import java.util.LinkedHashSet; +import java.util.List; public class TestBeanFactory { @@ -112,6 +120,10 @@ public static MachineEvent createWithdrawalEvent() { } public static MachineEvent createWithdrawalSucceeded() { + return createWithdrawalSucceeded(67L); + } + + public static MachineEvent createWithdrawalSucceeded(Long eventId) { dev.vality.fistful.withdrawal.Change change = new dev.vality.fistful.withdrawal.Change(); change.setStatusChanged(new StatusChange().setStatus(Status.succeeded(new Succeeded()))); @@ -122,11 +134,30 @@ public static MachineEvent createWithdrawalSucceeded() { return machineEvent( WITHDRAWAL_ID, - 67L, + eventId, new ThriftSerializer<>(), timestampedChange); } + public static WithdrawalState createWithdrawalState() { + return new WithdrawalState() + .setId(WITHDRAWAL_ID) + .setDestinationId(DESTINATION) + .setExternalId("extId") + .setWalletId(SOURCE_WALLET_ID) + .setPartyId(PARTY_ID) + .setCreatedAt("2025-03-22T06:12:27Z") + .setDomainRevision(1L) + .setBody(createCash(1000)); + } + + public static WithdrawalState createWithdrawalStateWithNewBody() { + return createWithdrawalState() + .setNewBody(createCash(1500, "USD")) + .setEffectiveFinalCashFlow(new FinalCashFlow() + .setPostings(List.of(createFeePosting()))); + } + public static WebHookModel createWebhookModel() { LinkedHashSet eventTypes = new LinkedHashSet<>(); eventTypes.add(EventType.WITHDRAWAL_CREATED); @@ -153,4 +184,26 @@ private static MachineEvent machineEvent( .setCreatedAt("2016-03-22T06:12:27Z") .setData(Value.bin(depositChangeSerializer.serialize("", change))); } + + private static Cash createCash(long amount) { + return createCash(amount, "RUB"); + } + + private static Cash createCash(long amount, String currencyCode) { + Cash body = new Cash(); + body.setAmount(amount); + CurrencyRef currency = new CurrencyRef(); + currency.setSymbolicCode(currencyCode); + body.setCurrency(currency); + return body; + } + + private static FinalCashFlowPosting createFeePosting() { + return new FinalCashFlowPosting() + .setSource(new FinalCashFlowAccount() + .setAccountType(CashFlowAccount.wallet(WalletCashFlowAccount.sender_source))) + .setDestination(new FinalCashFlowAccount() + .setAccountType(CashFlowAccount.system(SystemCashFlowAccount.settlement))) + .setVolume(createCash(25)); + } } diff --git a/src/test/java/dev/vality/wallets/hooker/handler/WithdrawalEventHandlerTest.java b/src/test/java/dev/vality/wallets/hooker/handler/WithdrawalEventHandlerTest.java index 32c0254..cbff401 100644 --- a/src/test/java/dev/vality/wallets/hooker/handler/WithdrawalEventHandlerTest.java +++ b/src/test/java/dev/vality/wallets/hooker/handler/WithdrawalEventHandlerTest.java @@ -1,14 +1,17 @@ package dev.vality.wallets.hooker.handler; +import dev.vality.fistful.withdrawal.ManagementSrv; import dev.vality.machinegun.eventsink.MachineEvent; import dev.vality.wallets.hooker.config.PostgresqlSpringBootITest; import dev.vality.wallets.hooker.dao.webhook.WebHookDao; import dev.vality.wallets.hooker.domain.WebHookModel; import dev.vality.wallets.hooker.service.WebHookMessageSenderService; import dev.vality.wallets.hooker.service.WithdrawalClient; -import dev.vality.fistful.withdrawal.ManagementSrv; import dev.vality.wallets.hooker.service.kafka.WithdrawalEventService; +import dev.vality.webhook.dispatcher.WebhookMessage; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -16,8 +19,11 @@ import java.util.concurrent.CountDownLatch; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @PostgresqlSpringBootITest class WithdrawalEventHandlerTest { @@ -40,6 +46,8 @@ class WithdrawalEventHandlerTest { @Test void handleWithdrawalCreatedAndAndStatusChange() throws InterruptedException { WebHookModel webhook = TestBeanFactory.createWebhookModel(); + when(withdrawalClient.getWithdrawalInfo(eq(TestBeanFactory.WITHDRAWAL_ID), anyLong())) + .thenReturn(TestBeanFactory.createWithdrawalState()); webHookDao.create(webhook); @@ -56,4 +64,26 @@ void handleWithdrawalCreatedAndAndStatusChange() throws InterruptedException { latch.await(); } + + @Test + void handleWithdrawalSucceededWithNewBodyInSucceededWebhook() { + WebHookModel webhook = TestBeanFactory.createWebhookModel(); + when(withdrawalClient.getWithdrawalInfo(TestBeanFactory.WITHDRAWAL_ID, 68L)) + .thenReturn(TestBeanFactory.createWithdrawalStateWithNewBody()); + + webHookDao.create(webhook); + + withdrawalEventService.handleEvents(List.of(TestBeanFactory.createWithdrawalEvent())); + withdrawalEventService.handleEvents(List.of(TestBeanFactory.createWithdrawalSucceeded(68L))); + + ArgumentCaptor captor = ArgumentCaptor.forClass(WebhookMessage.class); + verify(webHookMessageSenderService, timeout(1000L).times(2)) + .send(captor.capture()); + Assertions.assertTrue(captor.getAllValues().stream() + .map(WebhookMessage::getRequestBody) + .map(String::new) + .anyMatch(body -> body.contains("\"eventType\":\"WithdrawalSucceeded\"") + && body.contains("\"body\":{\"amount\":1500,\"currency\":\"USD\"}") + && body.contains("\"fee\":{\"amount\":25,\"currency\":\"RUB\"}"))); + } }