Skip to content

Commit 84654e2

Browse files
feat(s4): infrastructure persistence tests — 32 adapter ITs (STA-135)
ChainTransferPersistenceAdapterIT (10 tests): CRUD, findByPaymentIdAndType, findByStatus, full happy path, resubmission path, failure path, return transfer with parent id, nullable chain id. WalletPersistenceAdapterIT (7 tests): CRUD, findByAddress, findByChainIdAndPurpose (only active), deactivate update, unique constraint. WalletBalancePersistenceAdapterIT (8 tests): CRUD, findByWalletIdAndStablecoin, findByWalletId, reserve, release + confirmDebit, unique constraint, decimal precision (8 places). TransferParticipantPersistenceAdapterIT (4 tests): CRUD, multiple participants (INPUT/OUTPUT/FEE), all participant types enum round-trip. TransferLifecycleEventPersistenceAdapterIT (4 tests): CRUD, multiple events, event with participant details. V4 migration: fix NUMERIC(20,0) → BIGINT for block_number, nonce, last_indexed_block, current_nonce (Hibernate maps Long to BIGINT). Closes STA-135 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a6ac620 commit 84654e2

6 files changed

Lines changed: 700 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package com.stablecoin.payments.custody.infrastructure.persistence;
2+
3+
import com.stablecoin.payments.custody.AbstractIntegrationTest;
4+
import com.stablecoin.payments.custody.domain.model.ChainTransfer;
5+
import com.stablecoin.payments.custody.domain.model.TransferStatus;
6+
import com.stablecoin.payments.custody.domain.model.TransferType;
7+
import com.stablecoin.payments.custody.domain.model.Wallet;
8+
import com.stablecoin.payments.custody.domain.port.ChainTransferRepository;
9+
import com.stablecoin.payments.custody.domain.port.WalletRepository;
10+
import org.junit.jupiter.api.DisplayName;
11+
import org.junit.jupiter.api.Test;
12+
import org.springframework.beans.factory.annotation.Autowired;
13+
14+
import java.math.BigDecimal;
15+
import java.time.Instant;
16+
import java.util.UUID;
17+
18+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.AMOUNT;
19+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.CHAIN_BASE;
20+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.CORRELATION_ID;
21+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.FROM_ADDRESS;
22+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.PARENT_TRANSFER_ID;
23+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.PAYMENT_ID;
24+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.TO_ADDRESS;
25+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.TX_HASH;
26+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.TX_HASH_RESUBMIT;
27+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.USDC;
28+
import static com.stablecoin.payments.custody.fixtures.WalletFixtures.anActiveWallet;
29+
import static org.assertj.core.api.Assertions.assertThat;
30+
31+
@DisplayName("ChainTransferPersistenceAdapter IT")
32+
class ChainTransferPersistenceAdapterIT extends AbstractIntegrationTest {
33+
34+
@Autowired
35+
private ChainTransferRepository adapter;
36+
37+
@Autowired
38+
private WalletRepository walletAdapter;
39+
40+
// ── Save & Retrieve ──────────────────────────────────────────────────
41+
42+
@Test
43+
@DisplayName("should save and retrieve pending transfer with null chain id")
44+
void shouldSaveAndRetrievePendingTransfer() {
45+
var wallet = saveWallet();
46+
var transfer = createPendingTransfer(wallet.walletId());
47+
var saved = adapter.save(transfer);
48+
49+
assertThat(adapter.findById(saved.transferId())).isPresent().get()
50+
.usingRecursiveComparison()
51+
.withComparatorForType(BigDecimal::compareTo, BigDecimal.class)
52+
.ignoringFieldsOfTypes(Instant.class)
53+
.isEqualTo(saved);
54+
}
55+
56+
@Test
57+
@DisplayName("should find by payment id and type")
58+
void shouldFindByPaymentIdAndType() {
59+
var wallet = saveWallet();
60+
var transfer = createPendingTransfer(wallet.walletId());
61+
var saved = adapter.save(transfer);
62+
63+
assertThat(adapter.findByPaymentIdAndType(PAYMENT_ID, TransferType.FORWARD)).isPresent().get()
64+
.usingRecursiveComparison()
65+
.withComparatorForType(BigDecimal::compareTo, BigDecimal.class)
66+
.ignoringFieldsOfTypes(Instant.class)
67+
.isEqualTo(saved);
68+
}
69+
70+
@Test
71+
@DisplayName("should return empty when id not found")
72+
void shouldReturnEmptyWhenIdNotFound() {
73+
assertThat(adapter.findById(UUID.randomUUID())).isEmpty();
74+
}
75+
76+
@Test
77+
@DisplayName("should return empty when payment id and type not found")
78+
void shouldReturnEmptyWhenPaymentIdAndTypeNotFound() {
79+
assertThat(adapter.findByPaymentIdAndType(UUID.randomUUID(), TransferType.FORWARD)).isEmpty();
80+
}
81+
82+
@Test
83+
@DisplayName("should find by status")
84+
void shouldFindByStatus() {
85+
var wallet = saveWallet();
86+
var transfer = createPendingTransfer(wallet.walletId());
87+
adapter.save(transfer);
88+
89+
assertThat(adapter.findByStatus(TransferStatus.PENDING)).hasSize(1);
90+
assertThat(adapter.findByStatus(TransferStatus.CONFIRMED)).isEmpty();
91+
}
92+
93+
// ── State Machine Happy Path ─────────────────────────────────────────
94+
95+
@Test
96+
@DisplayName("should update transfer through full happy path to CONFIRMED")
97+
void shouldUpdateTransferThroughFullHappyPath() {
98+
var wallet = saveWallet();
99+
var transfer = createPendingTransfer(wallet.walletId());
100+
var saved = adapter.save(transfer);
101+
102+
saved = adapter.save(saved.selectChain(CHAIN_BASE));
103+
saved = adapter.save(saved.startSigning(42L));
104+
saved = adapter.save(saved.submit(TX_HASH));
105+
saved = adapter.save(saved.startConfirming());
106+
var expected = adapter.save(
107+
saved.confirm(12345L, 15, new BigDecimal("21000"), new BigDecimal("25.500000000")));
108+
109+
assertThat(adapter.findById(transfer.transferId())).isPresent().get()
110+
.usingRecursiveComparison()
111+
.withComparatorForType(BigDecimal::compareTo, BigDecimal.class)
112+
.ignoringFieldsOfTypes(Instant.class)
113+
.isEqualTo(expected);
114+
}
115+
116+
@Test
117+
@DisplayName("should update transfer through resubmission path")
118+
void shouldUpdateTransferThroughResubmissionPath() {
119+
var wallet = saveWallet();
120+
var transfer = createPendingTransfer(wallet.walletId());
121+
var saved = adapter.save(transfer);
122+
123+
saved = adapter.save(saved.selectChain(CHAIN_BASE));
124+
saved = adapter.save(saved.startSigning(42L));
125+
saved = adapter.save(saved.submit(TX_HASH));
126+
saved = adapter.save(saved.markForResubmission());
127+
var expected = adapter.save(saved.resubmit(TX_HASH_RESUBMIT));
128+
129+
assertThat(adapter.findById(transfer.transferId())).isPresent().get()
130+
.usingRecursiveComparison()
131+
.withComparatorForType(BigDecimal::compareTo, BigDecimal.class)
132+
.ignoringFieldsOfTypes(Instant.class)
133+
.isEqualTo(expected);
134+
}
135+
136+
// ── Failure Path ─────────────────────────────────────────────────────
137+
138+
@Test
139+
@DisplayName("should update transfer through failure path")
140+
void shouldUpdateTransferThroughFailurePath() {
141+
var wallet = saveWallet();
142+
var transfer = createPendingTransfer(wallet.walletId());
143+
var saved = adapter.save(transfer);
144+
saved = adapter.save(saved.selectChain(CHAIN_BASE));
145+
var expected = adapter.save(saved.fail("Insufficient gas", "GAS_LIMIT_EXCEEDED"));
146+
147+
assertThat(adapter.findById(transfer.transferId())).isPresent().get()
148+
.usingRecursiveComparison()
149+
.withComparatorForType(BigDecimal::compareTo, BigDecimal.class)
150+
.ignoringFieldsOfTypes(Instant.class)
151+
.isEqualTo(expected);
152+
}
153+
154+
// ── Return Transfer ──────────────────────────────────────────────────
155+
156+
@Test
157+
@DisplayName("should persist return transfer with parent id")
158+
void shouldPersistReturnTransferWithParentId() {
159+
var wallet = saveWallet();
160+
var transfer = ChainTransfer.initiate(
161+
PAYMENT_ID, CORRELATION_ID, TransferType.RETURN, PARENT_TRANSFER_ID,
162+
USDC, AMOUNT, wallet.walletId(), TO_ADDRESS, FROM_ADDRESS);
163+
var saved = adapter.save(transfer);
164+
165+
assertThat(adapter.findByPaymentIdAndType(PAYMENT_ID, TransferType.RETURN)).isPresent().get()
166+
.usingRecursiveComparison()
167+
.withComparatorForType(BigDecimal::compareTo, BigDecimal.class)
168+
.ignoringFieldsOfTypes(Instant.class)
169+
.isEqualTo(saved);
170+
}
171+
172+
// ── Helpers ───────────────────────────────────────────────────────────
173+
174+
private Wallet saveWallet() {
175+
return walletAdapter.save(anActiveWallet());
176+
}
177+
178+
private ChainTransfer createPendingTransfer(UUID fromWalletId) {
179+
return ChainTransfer.initiate(
180+
PAYMENT_ID, CORRELATION_ID, TransferType.FORWARD, null,
181+
USDC, AMOUNT, fromWalletId, TO_ADDRESS, FROM_ADDRESS);
182+
}
183+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.stablecoin.payments.custody.infrastructure.persistence;
2+
3+
import com.stablecoin.payments.custody.AbstractIntegrationTest;
4+
import com.stablecoin.payments.custody.domain.model.ChainTransfer;
5+
import com.stablecoin.payments.custody.domain.model.TransferLifecycleEvent;
6+
import com.stablecoin.payments.custody.domain.model.TransferType;
7+
import com.stablecoin.payments.custody.domain.model.Wallet;
8+
import com.stablecoin.payments.custody.domain.port.ChainTransferRepository;
9+
import com.stablecoin.payments.custody.domain.port.TransferLifecycleEventRepository;
10+
import com.stablecoin.payments.custody.domain.port.WalletRepository;
11+
import org.junit.jupiter.api.DisplayName;
12+
import org.junit.jupiter.api.Test;
13+
import org.springframework.beans.factory.annotation.Autowired;
14+
15+
import java.time.Instant;
16+
import java.util.UUID;
17+
18+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.AMOUNT;
19+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.CORRELATION_ID;
20+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.FROM_ADDRESS;
21+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.PAYMENT_ID;
22+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.TO_ADDRESS;
23+
import static com.stablecoin.payments.custody.fixtures.ChainTransferFixtures.USDC;
24+
import static com.stablecoin.payments.custody.fixtures.WalletFixtures.anActiveWallet;
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
@DisplayName("TransferLifecycleEventPersistenceAdapter IT")
28+
class TransferLifecycleEventPersistenceAdapterIT extends AbstractIntegrationTest {
29+
30+
@Autowired
31+
private TransferLifecycleEventRepository adapter;
32+
33+
@Autowired
34+
private ChainTransferRepository transferAdapter;
35+
36+
@Autowired
37+
private WalletRepository walletAdapter;
38+
39+
// ── Save & Retrieve ──────────────────────────────────────────────────
40+
41+
@Test
42+
@DisplayName("should save and retrieve by transfer id")
43+
void shouldSaveAndRetrieveByTransferId() {
44+
var wallet = saveWallet();
45+
var transfer = saveTransfer(wallet.walletId());
46+
47+
var event = TransferLifecycleEvent.record(transfer.transferId(), "PENDING");
48+
var saved = adapter.save(event);
49+
50+
var results = adapter.findByTransferId(transfer.transferId());
51+
assertThat(results).hasSize(1);
52+
assertThat(results.getFirst())
53+
.usingRecursiveComparison()
54+
.ignoringFieldsOfTypes(Instant.class)
55+
.isEqualTo(saved);
56+
}
57+
58+
@Test
59+
@DisplayName("should return empty list for unknown transfer id")
60+
void shouldReturnEmptyListForUnknownTransferId() {
61+
assertThat(adapter.findByTransferId(UUID.randomUUID())).isEmpty();
62+
}
63+
64+
// ── Multiple Events ──────────────────────────────────────────────────
65+
66+
@Test
67+
@DisplayName("should save multiple events for same transfer")
68+
void shouldSaveMultipleEventsForSameTransfer() {
69+
var wallet = saveWallet();
70+
var transfer = saveTransfer(wallet.walletId());
71+
72+
adapter.save(TransferLifecycleEvent.record(transfer.transferId(), "PENDING"));
73+
adapter.save(TransferLifecycleEvent.record(transfer.transferId(), "CHAIN_SELECTED"));
74+
adapter.save(TransferLifecycleEvent.record(transfer.transferId(), "SIGNING"));
75+
76+
assertThat(adapter.findByTransferId(transfer.transferId())).hasSize(3);
77+
}
78+
79+
// ── Event with Participant Details ────────────────────────────────────
80+
81+
@Test
82+
@DisplayName("should persist event with participant details")
83+
void shouldPersistEventWithParticipantDetails() {
84+
var wallet = saveWallet();
85+
var transfer = saveTransfer(wallet.walletId());
86+
87+
var event = TransferLifecycleEvent.record(
88+
transfer.transferId(), "SUBMITTED", "INPUT", FROM_ADDRESS);
89+
var saved = adapter.save(event);
90+
91+
var results = adapter.findByTransferId(transfer.transferId());
92+
assertThat(results).hasSize(1);
93+
assertThat(results.getFirst())
94+
.usingRecursiveComparison()
95+
.ignoringFieldsOfTypes(Instant.class)
96+
.isEqualTo(saved);
97+
}
98+
99+
// ── Helpers ───────────────────────────────────────────────────────────
100+
101+
private Wallet saveWallet() {
102+
return walletAdapter.save(anActiveWallet());
103+
}
104+
105+
private ChainTransfer saveTransfer(UUID fromWalletId) {
106+
return transferAdapter.save(ChainTransfer.initiate(
107+
PAYMENT_ID, CORRELATION_ID, TransferType.FORWARD, null,
108+
USDC, AMOUNT, fromWalletId, TO_ADDRESS, FROM_ADDRESS));
109+
}
110+
}

0 commit comments

Comments
 (0)