-
Notifications
You must be signed in to change notification settings - Fork 0
feat(s3): infrastructure persistence tests — 21 adapter ITs (STA-123) #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
177 changes: 177 additions & 0 deletions
177
...ecoin/payments/onramp/infrastructure/persistence/CollectionOrderPersistenceAdapterIT.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| package com.stablecoin.payments.onramp.infrastructure.persistence; | ||
|
|
||
| import com.stablecoin.payments.onramp.AbstractIntegrationTest; | ||
| import com.stablecoin.payments.onramp.domain.model.CollectionOrder; | ||
| import com.stablecoin.payments.onramp.domain.port.CollectionOrderRepository; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
| import org.springframework.dao.DataIntegrityViolationException; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.Instant; | ||
| import java.util.UUID; | ||
|
|
||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.ERROR_CODE; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.FAILURE_REASON; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.PSP_REFERENCE; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.aBankAccount; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.aCollectedMoney; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.aMoney; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.aPaymentRail; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.aPendingOrder; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.aPspIdentifier; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
| import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
|
||
| @DisplayName("CollectionOrderPersistenceAdapter IT") | ||
| class CollectionOrderPersistenceAdapterIT extends AbstractIntegrationTest { | ||
|
|
||
| @Autowired | ||
| private CollectionOrderRepository adapter; | ||
|
|
||
| // ── Save & Retrieve ────────────────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should save and retrieve pending order") | ||
| void shouldSaveAndRetrievePendingOrder() { | ||
| var order = aPendingOrder(); | ||
| var saved = adapter.save(order); | ||
|
|
||
| assertThat(adapter.findById(saved.collectionId())).isPresent().get() | ||
| .usingRecursiveComparison() | ||
| .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) | ||
| .ignoringFieldsOfTypes(Instant.class) | ||
| .isEqualTo(saved); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should find by payment id") | ||
| void shouldFindByPaymentId() { | ||
| var order = aPendingOrder(); | ||
| var saved = adapter.save(order); | ||
|
|
||
| assertThat(adapter.findByPaymentId(saved.paymentId())).isPresent().get() | ||
| .usingRecursiveComparison() | ||
| .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) | ||
| .ignoringFieldsOfTypes(Instant.class) | ||
| .isEqualTo(saved); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should return empty when id not found") | ||
| void shouldReturnEmptyWhenIdNotFound() { | ||
| assertThat(adapter.findById(UUID.randomUUID())).isEmpty(); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should return empty when payment id not found") | ||
| void shouldReturnEmptyWhenPaymentIdNotFound() { | ||
| assertThat(adapter.findByPaymentId(UUID.randomUUID())).isEmpty(); | ||
| } | ||
|
|
||
| // ── Unique Constraints ─────────────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should enforce unique payment id constraint") | ||
| void shouldEnforceUniquePaymentIdConstraint() { | ||
| var paymentId = UUID.randomUUID(); | ||
| var order1 = CollectionOrder.initiate( | ||
| paymentId, UUID.randomUUID(), aMoney(), aPaymentRail(), aPspIdentifier(), aBankAccount()); | ||
| adapter.save(order1); | ||
|
|
||
| var order2 = CollectionOrder.initiate( | ||
| paymentId, UUID.randomUUID(), aMoney(), aPaymentRail(), aPspIdentifier(), aBankAccount()); | ||
|
|
||
| assertThatThrownBy(() -> adapter.save(order2)) | ||
| .isInstanceOf(DataIntegrityViolationException.class); | ||
| } | ||
|
|
||
| // ── State Machine Happy Path ───────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should update order through full happy path to COLLECTED") | ||
| void shouldUpdateOrderThroughFullHappyPath() { | ||
| var order = aPendingOrder(); | ||
| var saved = adapter.save(order); | ||
|
|
||
| var initiated = saved.initiatePayment(); | ||
| var savedInitiated = adapter.save(initiated); | ||
|
|
||
| var awaiting = savedInitiated.awaitConfirmation(PSP_REFERENCE); | ||
| var savedAwaiting = adapter.save(awaiting); | ||
|
|
||
| var collected = savedAwaiting.confirmCollection(aCollectedMoney()); | ||
| var expected = adapter.save(collected); | ||
|
|
||
| assertThat(adapter.findById(order.collectionId())).isPresent().get() | ||
| .usingRecursiveComparison() | ||
| .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) | ||
| .ignoringFieldsOfTypes(Instant.class) | ||
| .isEqualTo(expected); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should update order through refund path") | ||
| void shouldUpdateOrderThroughRefundPath() { | ||
| var saved = adapter.save(aPendingOrder()); | ||
| saved = adapter.save(saved.initiatePayment()); | ||
| saved = adapter.save(saved.awaitConfirmation(PSP_REFERENCE)); | ||
| saved = adapter.save(saved.confirmCollection(aCollectedMoney())); | ||
| saved = adapter.save(saved.initiateRefund()); | ||
| saved = adapter.save(saved.startRefundProcessing()); | ||
| var expected = adapter.save(saved.completeRefund()); | ||
|
|
||
| assertThat(adapter.findById(expected.collectionId())).isPresent().get() | ||
| .usingRecursiveComparison() | ||
| .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) | ||
| .ignoringFieldsOfTypes(Instant.class) | ||
| .isEqualTo(expected); | ||
| } | ||
|
|
||
| // ── Failure Paths ──────────────────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should update order through failure path") | ||
| void shouldUpdateOrderThroughFailurePath() { | ||
| var saved = adapter.save(aPendingOrder()); | ||
| var expected = adapter.save(saved.failCollection(FAILURE_REASON, ERROR_CODE)); | ||
|
|
||
| assertThat(adapter.findById(expected.collectionId())).isPresent().get() | ||
| .usingRecursiveComparison() | ||
| .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) | ||
| .ignoringFieldsOfTypes(Instant.class) | ||
| .isEqualTo(expected); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should persist amount mismatch and manual review") | ||
| void shouldPersistAmountMismatchAndManualReview() { | ||
| var saved = adapter.save(aPendingOrder()); | ||
| saved = adapter.save(saved.initiatePayment()); | ||
| saved = adapter.save(saved.awaitConfirmation(PSP_REFERENCE)); | ||
| saved = adapter.save(saved.detectAmountMismatch()); | ||
| var expected = adapter.save(saved.escalateToManualReview()); | ||
|
|
||
| assertThat(adapter.findById(expected.collectionId())).isPresent().get() | ||
| .usingRecursiveComparison() | ||
| .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) | ||
| .ignoringFieldsOfTypes(Instant.class) | ||
| .isEqualTo(expected); | ||
| } | ||
|
|
||
| // ── Nullable Fields ────────────────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should persist nullable fields correctly for pending order") | ||
| void shouldPersistNullableFieldsCorrectly() { | ||
| var order = aPendingOrder(); | ||
| var saved = adapter.save(order); | ||
|
|
||
| assertThat(adapter.findById(saved.collectionId())).isPresent().get() | ||
| .usingRecursiveComparison() | ||
| .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) | ||
| .ignoringFieldsOfTypes(Instant.class) | ||
| .isEqualTo(saved); | ||
| } | ||
| } |
123 changes: 123 additions & 0 deletions
123
...lecoin/payments/onramp/infrastructure/persistence/PspTransactionPersistenceAdapterIT.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| package com.stablecoin.payments.onramp.infrastructure.persistence; | ||
|
|
||
| import com.stablecoin.payments.onramp.AbstractIntegrationTest; | ||
| import com.stablecoin.payments.onramp.domain.model.CollectionOrder; | ||
| import com.stablecoin.payments.onramp.domain.model.PspTransaction; | ||
| import com.stablecoin.payments.onramp.domain.model.PspTransactionDirection; | ||
| import com.stablecoin.payments.onramp.domain.port.CollectionOrderRepository; | ||
| import com.stablecoin.payments.onramp.domain.port.PspTransactionRepository; | ||
| import org.junit.jupiter.api.DisplayName; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.springframework.beans.factory.annotation.Autowired; | ||
|
|
||
| import java.math.BigDecimal; | ||
| import java.time.Instant; | ||
| import java.util.UUID; | ||
|
|
||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.aMoney; | ||
| import static com.stablecoin.payments.onramp.fixtures.CollectionOrderFixtures.aPendingOrder; | ||
| import static org.assertj.core.api.Assertions.assertThat; | ||
|
|
||
| @DisplayName("PspTransactionPersistenceAdapter IT") | ||
| class PspTransactionPersistenceAdapterIT extends AbstractIntegrationTest { | ||
|
|
||
| @Autowired | ||
| private PspTransactionRepository adapter; | ||
|
|
||
| @Autowired | ||
| private CollectionOrderRepository collectionOrderAdapter; | ||
|
|
||
| // ── Save & Retrieve ────────────────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should save and retrieve by collection id") | ||
| void shouldSaveAndRetrieveByCollectionId() { | ||
| var order = saveCollectionOrder(); | ||
| var txn = PspTransaction.create( | ||
| order.collectionId(), "Stripe", "pi_123", PspTransactionDirection.DEBIT, | ||
| "payment_intent.succeeded", aMoney(), "succeeded", "{\"id\":\"pi_123\"}"); | ||
| adapter.save(txn); | ||
|
|
||
| var results = adapter.findByCollectionId(order.collectionId()); | ||
| assertThat(results).hasSize(1); | ||
| assertThat(results.getFirst()) | ||
| .usingRecursiveComparison() | ||
| .withComparatorForType(BigDecimal::compareTo, BigDecimal.class) | ||
| .ignoringFieldsOfTypes(Instant.class) | ||
| .ignoringFields("rawResponse") | ||
| .isEqualTo(txn); | ||
| } | ||
|
|
||
| @Test | ||
| @DisplayName("should return empty list for unknown collection id") | ||
| void shouldReturnEmptyListForUnknownCollectionId() { | ||
| assertThat(adapter.findByCollectionId(UUID.randomUUID())).isEmpty(); | ||
| } | ||
|
|
||
| // ── Append-Only Pattern ────────────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should save multiple transactions for same collection") | ||
| void shouldSaveMultipleTransactionsForSameCollection() { | ||
| var order = saveCollectionOrder(); | ||
|
|
||
| var txn1 = PspTransaction.create( | ||
| order.collectionId(), "Stripe", "pi_123", PspTransactionDirection.DEBIT, | ||
| "payment_intent.created", aMoney(), "requires_payment_method", "{\"status\":\"created\"}"); | ||
| adapter.save(txn1); | ||
|
|
||
| var txn2 = PspTransaction.create( | ||
| order.collectionId(), "Stripe", "pi_123", PspTransactionDirection.DEBIT, | ||
| "payment_intent.succeeded", aMoney(), "succeeded", "{\"status\":\"succeeded\"}"); | ||
| adapter.save(txn2); | ||
|
|
||
| var txn3 = PspTransaction.create( | ||
| order.collectionId(), "Stripe", "re_456", PspTransactionDirection.CREDIT, | ||
| "refund.created", aMoney(), "pending", "{\"status\":\"pending\"}"); | ||
| adapter.save(txn3); | ||
|
|
||
| assertThat(adapter.findByCollectionId(order.collectionId())).hasSize(3); | ||
| } | ||
|
|
||
| // ── JSONB Round-Trip ───────────────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should persist raw response JSONB") | ||
| void shouldPersistRawResponseJsonb() { | ||
| var order = saveCollectionOrder(); | ||
| var txn = PspTransaction.create( | ||
| order.collectionId(), "Stripe", "pi_789", PspTransactionDirection.DEBIT, | ||
| "charge.succeeded", aMoney(), "succeeded", | ||
| "{\"id\":\"ch_abc\",\"amount\":100000,\"currency\":\"usd\"}"); | ||
| adapter.save(txn); | ||
|
|
||
| var results = adapter.findByCollectionId(order.collectionId()); | ||
| assertThat(results).hasSize(1); | ||
| assertThat(results.getFirst().rawResponse()).contains("ch_abc"); | ||
| } | ||
|
|
||
| // ── Enum Round-Trip ────────────────────────────────────────────────── | ||
|
|
||
| @Test | ||
| @DisplayName("should persist all direction enum values") | ||
| void shouldPersistAllDirectionEnumValues() { | ||
| var order = saveCollectionOrder(); | ||
|
|
||
| for (var direction : PspTransactionDirection.values()) { | ||
| var txn = PspTransaction.create( | ||
| order.collectionId(), "Stripe", "ref_" + direction.name(), | ||
| direction, "event_" + direction.name(), aMoney(), | ||
| "completed", "{}"); | ||
| adapter.save(txn); | ||
| } | ||
|
|
||
| assertThat(adapter.findByCollectionId(order.collectionId())) | ||
| .hasSize(PspTransactionDirection.values().length); | ||
| } | ||
|
|
||
| // ── Helpers ─────────────────────────────────────────────────────────── | ||
|
|
||
| private CollectionOrder saveCollectionOrder() { | ||
| return collectionOrderAdapter.save(aPendingOrder()); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assert persisted directions, not only row count.
Line 114 only proves inserts happened. If the enum mapping collapses every value to the same persisted value, this test still passes. Extract the direction field so the round-trip is actually covered.
Patch
🤖 Prompt for AI Agents