feat: close overseas wallet and pallet cost flow

This commit is contained in:
2026-09-05 22:26:25 +08:00
parent f62283fb7f
commit 5fbd2bbd04
69 changed files with 1598 additions and 53 deletions
@@ -43,8 +43,8 @@ class FinanceSqlCommentContractTest {
missing.add(currentTable + "." + column.group(1));
}
}
assertThat(tableCount).isEqualTo(21);
assertThat(columnCount).isEqualTo(330);
assertThat(tableCount).isEqualTo(23);
assertThat(columnCount).isEqualTo(350);
assertThat(missing).as("缺少中文业务注释的Finance字段").isEmpty();
}
@@ -0,0 +1,100 @@
package com.baiyue.finance;
import com.baiyue.common.event.EventEnvelope;
import com.baiyue.common.event.EventPayloadCanonicalizer;
import com.baiyue.common.event.EventPayloadHash;
import com.baiyue.common.event.EventTypes;
import com.baiyue.common.event.RegionCode;
import com.baiyue.common.event.WalletCommandRequestedV1;
import com.baiyue.finance.dto.FinanceWalletDtos.WalletHold;
import com.baiyue.finance.service.FinanceEventException;
import com.baiyue.finance.service.FinanceWalletService;
import com.baiyue.finance.service.WalletCommandEventService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
class WalletCommandEventServiceTest {
private final JdbcTemplate jdbc = mock(JdbcTemplate.class);
private final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
private final FinanceWalletService wallet = mock(FinanceWalletService.class);
private final TransactionTemplate transactions = mock(TransactionTemplate.class);
private final WalletCommandEventService service = new WalletCommandEventService(jdbc, mapper, wallet, transactions);
@BeforeEach
void executeTransactionCallbacks() {
when(transactions.execute(any())).thenAnswer(invocation -> {
TransactionCallback<?> callback = invocation.getArgument(0);
return callback.doInTransaction(mock(TransactionStatus.class));
});
doAnswer(invocation -> {
Consumer<TransactionStatus> callback = invocation.getArgument(0);
callback.accept(mock(TransactionStatus.class));
return null;
}).when(transactions).executeWithoutResult(any());
}
@Test
void freezesWalletAndPublishesCompletedEvent() {
EventEnvelope envelope = envelope("FREEZE", 31L, new BigDecimal("88.50"));
when(jdbc.queryForList(startsWith("SELECT payload_hash"), eq(String.class), eq(envelope.eventId())))
.thenReturn(List.of());
when(jdbc.queryForObject(startsWith("SELECT COUNT(*)"), eq(Integer.class), anyString())).thenReturn(0);
when(wallet.freeze(eq(31L), eq("freeze-procurement-1"), any())).thenReturn(
new WalletHold(9L, 31L, "OVERSEAS_PROCUREMENT_FEE", "PO-1", new BigDecimal("88.50"),
new BigDecimal("80.00"), new BigDecimal("8.50"), "FROZEN", 71L, null,
null, null, null));
assertThat(service.consume(envelope)).isEqualTo(envelope.eventId());
verify(wallet).freeze(eq(31L), eq("freeze-procurement-1"), argThat(request ->
request.amount().compareTo(new BigDecimal("88.50")) == 0
&& request.businessNo().equals("PO-1")));
verify(jdbc).update(startsWith("INSERT IGNORE INTO fin_wallet_event_outbox"), any(Object[].class));
verify(jdbc).update(startsWith("UPDATE fin_wallet_command_inbox SET status="),
eq("SUCCEEDED"), contains("\"holdStatus\":\"FROZEN\""), eq(envelope.eventId()));
}
@Test
void rejectsCustomerMismatchBeforeTouchingWallet() {
EventEnvelope envelope = envelope("FREEZE", 31L, new BigDecimal("10.00"));
envelope = new EventEnvelope(envelope.eventId(), envelope.eventType(), envelope.eventVersion(),
envelope.occurredAt(), envelope.sourceRegion(), envelope.targetRegion(), "32", envelope.aggregateType(),
envelope.aggregateId(), envelope.aggregateVersion(), envelope.handoverVersion(), envelope.traceId(),
envelope.correlationId(), envelope.causationId(), envelope.payload(), envelope.payloadHash());
EventEnvelope mismatched = envelope;
assertThatThrownBy(() -> service.consume(mismatched))
.isInstanceOf(FinanceEventException.class)
.extracting(error -> ((FinanceEventException) error).code()).isEqualTo("FIN-EVT-007");
verifyNoInteractions(wallet);
}
private EventEnvelope envelope(String operation, long customerId, BigDecimal amount) {
WalletCommandRequestedV1 command = new WalletCommandRequestedV1(operation, customerId,
"OVERSEAS_PROCUREMENT_FEE", "PO-1", amount, "采购快递费",
"freeze-procurement-1", Instant.parse("2026-09-05T01:00:00Z"));
JsonNode payload = mapper.valueToTree(command);
return new EventEnvelope(UUID.randomUUID().toString(), EventTypes.WALLET_COMMAND_REQUESTED,
WalletCommandRequestedV1.VERSION, Instant.parse("2026-09-05T01:00:00Z"),
RegionCode.OVERSEAS, RegionCode.CN, String.valueOf(customerId), "WALLET_HOLD", "PO-1", 1,
null, UUID.randomUUID().toString(), UUID.randomUUID().toString(), null, payload,
EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload))).requireValid();
}
}