Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
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);
Comment on lines +101 to +115

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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
-        assertThat(adapter.findByCollectionId(order.collectionId()))
-                .hasSize(PspTransactionDirection.values().length);
+        assertThat(adapter.findByCollectionId(order.collectionId()))
+                .extracting(PspTransaction::direction)
+                .containsExactlyInAnyOrder(PspTransactionDirection.values());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-on-ramp/fiat-on-ramp/src/integration-test/java/com/stablecoin/payments/onramp/infrastructure/persistence/PspTransactionPersistenceAdapterIT.java`
around lines 101 - 115, The test shouldPersistAllDirectionEnumValues currently
only checks row count; update it to verify the persisted direction values
round-trip correctly by fetching transactions via
adapter.findByCollectionId(order.collectionId()), mapping each returned
PspTransaction's direction (e.g., txn.direction() or getDirection()) into a set,
and asserting that this set equals EnumSet.allOf(PspTransactionDirection.class)
(or the list of PspTransactionDirection.values()), ensuring every enum constant
was persisted distinctly; use the existing PspTransaction.create and
PspTransactionDirection symbols to build the expected values and compare against
the persisted directions.

}

// ── Helpers ───────────────────────────────────────────────────────────

private CollectionOrder saveCollectionOrder() {
return collectionOrderAdapter.save(aPendingOrder());
}
}
Loading
Loading