Skip to content

feat(s1): infrastructure persistence tests -- adapter + audit log ITs (STA-108)#109

Merged
Puneethkumarck merged 4 commits into
mainfrom
feature/STA-108-s1-persistence-tests
Mar 8, 2026
Merged

feat(s1): infrastructure persistence tests -- adapter + audit log ITs (STA-108)#109
Puneethkumarck merged 4 commits into
mainfrom
feature/STA-108-s1-persistence-tests

Conversation

@Puneethkumarck

@Puneethkumarck Puneethkumarck commented Mar 8, 2026

Copy link
Copy Markdown
Owner

Summary

Infrastructure persistence tests for the Payment Orchestrator (S1), covering PaymentPersistenceAdapter and PaymentAuditLogEntity with 20 integration tests using TestContainers PostgreSQL.

Related Issue

Closes STA-108

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 💥 Breaking change (fix or feature that changes existing behaviour)
  • ♻️ Refactor (no functional change)
  • 🔒 Security fix
  • 📦 Dependency update
  • 📝 Documentation only

What Changed

  • Added PaymentPersistenceAdapterIT (13 tests) — save/retrieve round-trip, idempotency key lookup, sender+state queries, optimistic locking, JSONB metadata, FX rate persistence, full state transition
  • Added PaymentAuditLogRepositoryIT (7 tests) — append-only insert, ordering, filtering, JSONB round-trip
  • Added AbstractIntegrationTest with TestContainers PostgreSQL singleton and @BeforeEach database cleanup
  • Added V3 migration to fix version column type from INT to BIGINT (matches Hibernate Long @Version)
  • Added TestKafkaConfig for IT context KafkaTemplate bean resolution

How Was It Tested

  • Unit tests added / updated
  • Integration tests added / updated
  • Business/acceptance tests added / updated
  • Manually tested locally with Docker Compose

Breaking Changes

N/A

Security Considerations

  • No sensitive data exposed in logs
  • Input validation in place for all new endpoints
  • No secrets or credentials introduced in code
  • N/A — no security-sensitive changes

Checklist

  • CI is green (unit, integration, business tests pass)
  • ./gradlew spotlessCheck passes
  • No wildcard imports introduced
  • CHANGELOG.md updated (for features and bug fixes)
  • Self-reviewed — I have read through the diff carefully

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests

    • Added comprehensive integration tests for audit logs and payment persistence, covering insertion, retrieval, ordering, metadata, state transitions, idempotency, optimistic locking, and full happy-path flows.
    • Improved test setup: automated DB cleanup between tests and dedicated Kafka test configuration.
  • Chores

    • Fixed payments table version column type.
    • Added repository/query support to fetch payments by sender ID and state.

@Puneethkumarck Puneethkumarck added the enhancement New feature or request label Mar 8, 2026
@Puneethkumarck Puneethkumarck changed the title feat(s1): infrastructure persistence tests (STA-108) feat(s1): infrastructure persistence tests -- adapter + audit log ITs (STA-108) Mar 8, 2026
@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@Puneethkumarck has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 15 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 60a6590c-2801-44c5-91e1-c9cb9b69ca0a

📥 Commits

Reviewing files that changed from the base of the PR and between 28b6eef and 5544d0e.

📒 Files selected for processing (8)
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/AbstractIntegrationTest.java
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/config/TestKafkaConfig.java
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentAuditLogRepositoryIT.java
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java
  • payment-orchestrator/payment-orchestrator/src/integration-test/resources/application-integration-test.yml
  • payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/domain/port/PaymentRepository.java
  • payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapter.java
  • payment-orchestrator/payment-orchestrator/src/main/resources/db/migration/V3__fix_version_column_type.sql

Walkthrough

Adds integration-test infrastructure (DB cleanup, Kafka test producer), extensive persistence integration tests for payments and audit logs, an application-integration-test.yml, a Flyway migration to change payments.version to BIGINT, and repository + adapter support for querying payments by senderId and state.

Changes

Cohort / File(s) Summary
Test base & DB cleanup
payment-orchestrator/.../AbstractIntegrationTest.java
Injects EntityManager and adds @BeforeEach cleanDatabase() to TRUNCATE payment_audit_log, payments, and orchestrator_outbox_record, then entityManager.clear() before each integration test.
Kafka test config
payment-orchestrator/.../config/TestKafkaConfig.java
New Spring @Configuration that provides @Primary ProducerFactory<String,Object> and KafkaTemplate<String,Object> wired from spring.kafka.bootstrap-servers and configured with StringSerializer and JsonSerializer for integration tests.
Integration tests: audit log
payment-orchestrator/.../infrastructure/persistence/PaymentAuditLogRepositoryIT.java
New integration test class covering insert/retrieve by id, ordered retrieval by occurredAt desc, paymentId isolation, and JSONB metadata persistence.
Integration tests: payments
payment-orchestrator/.../infrastructure/persistence/PaymentPersistenceAdapterIT.java
New integration test class covering save/retrieve, idempotency key lookups, sender+state queries, JSONB metadata, optimistic locking conflict, idempotency unique constraint, state transitions, FX lock, and full happy-path flow.
Integration test config
payment-orchestrator/.../resources/application-integration-test.yml
Adds Spring/JPA/Flyway/Kafka/outbox and logging settings used by integration tests (bean overriding, Hibernate validate, Flyway locations, outbox poll settings, serializers, logging levels).
DB migration
payment-orchestrator/.../db/migration/V3__fix_version_column_type.sql
Flyway migration to ALTER payments.version column TYPE to BIGINT to match Hibernate Long mapping.
Domain port change
payment-orchestrator/.../domain/port/PaymentRepository.java
Adds List<Payment> findBySenderIdAndState(UUID senderId, PaymentState state) to repository interface.
Persistence adapter
payment-orchestrator/.../infrastructure/persistence/PaymentPersistenceAdapter.java
Implements findBySenderIdAndState(UUID, PaymentState) delegating to JPA repo and mapping entities to domain model.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

service-s1, feature, test

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly identifies the main change: infrastructure persistence tests for Payment Orchestrator adapter and audit log, directly reflected in the changeset.
Description check ✅ Passed Description follows the template with all required sections completed: Summary, Related Issue, Type of Change, What Changed, How Was It Tested, Security Considerations, and Checklist.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/STA-108-s1-persistence-tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@Puneethkumarck Puneethkumarck added the phase-2 Core Payment Logic label Mar 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/AbstractIntegrationTest.java`:
- Around line 39-51: The call to entityManager.clear() in the `@BeforeEach`
cleanDatabase() is ineffective because Spring injects a shared EntityManager
proxy; either remove the entityManager.clear() line to avoid a false sense of
clearing the persistence context, or ensure it runs inside the same
transaction/persistence-context used by the repositories (e.g., make
cleanDatabase() `@Transactional` and call entityManager.clear() after the
TRUNCATE). Update the cleanDatabase() method accordingly and keep references to
the existing method name cleanDatabase(), the `@BeforeEach` annotated setup, and
the entityManager.clear() invocation when making the change.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentAuditLogRepositoryIT.java`:
- Around line 52-55: The test currently ignores all Instant fields which hides
mismatches for occurredAt; remove the .ignoringFieldsOfTypes(Instant.class) call
in the assertion chain in PaymentAuditLogRepositoryIT and instead add a
type-specific comparator for Instant that compares the persisted and expected
Instants at the database precision (e.g., truncating both to the DB precision
such as ChronoUnit.MILLIS) via AssertJ's usingComparatorForType for
Instant.class applied on the existing .usingRecursiveComparison() so that
occurredAt is asserted at DB precision rather than being completely ignored.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`:
- Around line 58-62: The assertions currently ignore all Instant fields (via
ignoringFieldsOfTypes(Instant.class)) which hides timestamp mapping regressions;
update the assertions around adapter.findById(saved.paymentId()) and the other
similar assertions (the ones comparing saved) to explicitly assert the Instant
fields createdAt, updatedAt, and expiresAt using either (a)
truncated-to-DB-precision comparison (e.g., truncate both expected and actual to
milliseconds or the DB precision) or (b) a tolerant comparator that allows a
small delta, instead of ignoring Instant.class; keep the BigDecimal comparator
for amounts and preserve recursive comparison for other fields but remove
ignoringFieldsOfTypes(Instant.class) and add targeted comparisons for those
timestamp fields so drift/loss fails the test.
- Around line 166-175: The test should assert the actual persisted JSON column,
not just the mapped domain metadata; after saving via adapter.save(payment) (use
saved.paymentId()), query the persistence side directly (e.g., via JdbcTemplate
or the PaymentEntity repository/finder) to fetch the raw metadata column for
that payment id and assert it is non-null and equals "{}" (or otherwise the
exact expected empty JSON representation) instead of only asserting
found.get().metadata() isEmpty(); update
shouldPersistEmptyMetadataAsEmptyJsonObject to perform that direct DB assertion
using the payment id returned by adapter.save.
- Around line 264-275: The test currently saves an already-COMPLETED fixture
(aCompletedPayment()) so it never exercises state transitions; change it to
start from an initiated fixture (e.g., aInitiatedPayment()), persist that
initial save via adapter.save(payment), then perform and persist each transition
(mutate the Payment to the next states and call adapter.save(...) for each
transition) until the payment reaches PaymentState.COMPLETED, and finally assert
that the retrieved payment equals the final saved version and that saved.state()
== PaymentState.COMPLETED; update the method
shouldUpdatePaymentThroughFullHappyPath to use aInitiatedPayment(),
adapter.save(...) for each step, and the same recursive comparison assertions.
- Around line 107-127: The test is calling
jpaRepository.findBySenderIdAndState(...) directly which bypasses
PaymentPersistenceAdapter; add a findBySenderIdAndState(UUID senderId,
PaymentState state) method to the PaymentRepository interface and implement a
corresponding public method on PaymentPersistenceAdapter that delegates to
repository.findBySenderIdAndState, then update the test to call
adapter.findBySenderIdAndState(SENDER_ID, PaymentState.INITIATED) (after saving
via adapter.save(payment1/2)) and assert the exact returned payments and their
PaymentState (not hasSizeGreaterThanOrEqualTo) to validate adapter
mapping/filtering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 430ab27f-3e2b-4ca0-ac94-d18be123e6ae

📥 Commits

Reviewing files that changed from the base of the PR and between 8a320a3 and 17e4835.

📒 Files selected for processing (6)
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/AbstractIntegrationTest.java
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/config/TestKafkaConfig.java
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentAuditLogRepositoryIT.java
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java
  • payment-orchestrator/payment-orchestrator/src/integration-test/resources/application-integration-test.yml
  • payment-orchestrator/payment-orchestrator/src/main/resources/db/migration/V3__fix_version_column_type.sql

Comment on lines +39 to +51
@Autowired
private EntityManager entityManager;

@BeforeEach
void cleanDatabase() {
jdbcTemplate.execute("""
TRUNCATE TABLE
payment_audit_log,
payments,
orchestrator_outbox_record
CASCADE
""");
entityManager.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

entityManager.clear() here likely is not clearing what this test expects.

Spring's shared EntityManager proxy delegates to the current transactional EntityManager and otherwise creates a new one per operation, so calling clear() in @BeforeEach is unlikely to evict the persistence context later used by repository calls unless the test itself is transactional. If the goal is to invalidate cached entities after the TRUNCATE, either do the clear inside the same active transaction or drop this line to avoid a false sense of isolation. That's an inference from Spring's shared EntityManager contract. (docs.spring.io)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/AbstractIntegrationTest.java`
around lines 39 - 51, The call to entityManager.clear() in the `@BeforeEach`
cleanDatabase() is ineffective because Spring injects a shared EntityManager
proxy; either remove the entityManager.clear() line to avoid a false sense of
clearing the persistence context, or ensure it runs inside the same
transaction/persistence-context used by the repositories (e.g., make
cleanDatabase() `@Transactional` and call entityManager.clear() after the
TRUNCATE). Update the cleanDatabase() method accordingly and keep references to
the existing method name cleanDatabase(), the `@BeforeEach` annotated setup, and
the entityManager.clear() invocation when making the change.

Comment on lines +52 to +55
assertThat(auditLogRepository.findById(saved.getLogId())).isPresent().get()
.usingRecursiveComparison()
.ignoringFieldsOfTypes(Instant.class)
.isEqualTo(expected);

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

Don’t exclude occurredAt from the repository round-trip assertion.

This still passes if occurredAt is truncated, defaulted, or mapped back incorrectly because every Instant is ignored. Compare it at database precision instead of dropping the type from the assertion.

As per coding guidelines, "Assert on meaningful values — avoid assertTrue(result != null)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentAuditLogRepositoryIT.java`
around lines 52 - 55, The test currently ignores all Instant fields which hides
mismatches for occurredAt; remove the .ignoringFieldsOfTypes(Instant.class) call
in the assertion chain in PaymentAuditLogRepositoryIT and instead add a
type-specific comparator for Instant that compares the persisted and expected
Instants at the database precision (e.g., truncating both to the DB precision
such as ChronoUnit.MILLIS) via AssertJ's usingComparatorForType for
Instant.class applied on the existing .usingRecursiveComparison() so that
occurredAt is asserted at DB precision rather than being completely ignored.

Comment on lines +58 to +62
assertThat(adapter.findById(saved.paymentId())).isPresent().get()
.usingRecursiveComparison()
.withComparatorForType(BigDecimal::compareTo, BigDecimal.class)
.ignoringFieldsOfTypes(Instant.class)
.isEqualTo(saved);

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

The round-trip assertions are blind to timestamp mapping regressions.

Ignoring every Instant means createdAt, updatedAt, and expiresAt can drift or be lost without failing this suite. For persistence tests, compare those fields with DB-precision truncation or a tolerant comparator instead of excluding the entire type.

As per coding guidelines, "Assert on meaningful values — avoid assertTrue(result != null)".

Also applies to: 71-75, 92-96, 238-242, 257-261, 270-274

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`
around lines 58 - 62, The assertions currently ignore all Instant fields (via
ignoringFieldsOfTypes(Instant.class)) which hides timestamp mapping regressions;
update the assertions around adapter.findById(saved.paymentId()) and the other
similar assertions (the ones comparing saved) to explicitly assert the Instant
fields createdAt, updatedAt, and expiresAt using either (a)
truncated-to-DB-precision comparison (e.g., truncate both expected and actual to
milliseconds or the DB precision) or (b) a tolerant comparator that allows a
small delta, instead of ignoring Instant.class; keep the BigDecimal comparator
for amounts and preserve recursive comparison for other fields but remove
ignoringFieldsOfTypes(Instant.class) and add targeted comparisons for those
timestamp fields so drift/loss fails the test.

Comment on lines +107 to +127
@Test
@DisplayName("should find payments by sender id and state")
void shouldFindPaymentsBySenderIdAndState() {
var payment1 = Payment.initiate(
"idem-sender-1", CORRELATION_ID, SENDER_ID, RECIPIENT_ID,
SOURCE_AMOUNT, SOURCE_CURRENCY, TARGET_CURRENCY, US_TO_DE);
var payment2 = Payment.initiate(
"idem-sender-2", CORRELATION_ID, SENDER_ID, RECIPIENT_ID,
SOURCE_AMOUNT, SOURCE_CURRENCY, TARGET_CURRENCY, US_TO_DE);
adapter.save(payment1);
adapter.save(payment2);

var results = jpaRepository.findBySenderIdAndState(SENDER_ID, PaymentState.INITIATED);
assertThat(results).hasSizeGreaterThanOrEqualTo(2);
}

@Test
@DisplayName("should return empty list when no payments match sender and state")
void shouldReturnEmptyListWhenNoPaymentsMatchSenderAndState() {
var results = jpaRepository.findBySenderIdAndState(UUID.randomUUID(), PaymentState.COMPLETED);
assertThat(results).isEmpty();

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether PaymentPersistenceAdapter exposes a sender/state query
# and where findBySenderIdAndState is currently used.

fd 'PaymentPersistenceAdapter.java' | while read -r file; do
  echo "== $file =="
  sed -n '1,260p' "$file"
done

echo
echo "== call sites for findBySenderIdAndState =="
rg -nP --type=java '\bfindBySenderIdAndState\s*\('

Repository: Puneethkumarck/stablebridge-platform

Length of output: 2681


Add findBySenderIdAndState to PaymentRepository and test through the adapter.

The test calls jpaRepository.findBySenderIdAndState(...) directly, bypassing the PaymentPersistenceAdapter under test. The adapter doesn't expose this query, so any adapter mapping or filtering bugs won't be caught. If this query is required, add it to the PaymentRepository interface and delegate through the adapter. Assert on the exact returned payments and their states instead of hasSizeGreaterThanOrEqualTo(2).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`
around lines 107 - 127, The test is calling
jpaRepository.findBySenderIdAndState(...) directly which bypasses
PaymentPersistenceAdapter; add a findBySenderIdAndState(UUID senderId,
PaymentState state) method to the PaymentRepository interface and implement a
corresponding public method on PaymentPersistenceAdapter that delegates to
repository.findBySenderIdAndState, then update the test to call
adapter.findBySenderIdAndState(SENDER_ID, PaymentState.INITIATED) (after saving
via adapter.save(payment1/2)) and assert the exact returned payments and their
PaymentState (not hasSizeGreaterThanOrEqualTo) to validate adapter
mapping/filtering.

Comment on lines +166 to +175
@Test
@DisplayName("should persist empty metadata as empty JSON object")
void shouldPersistEmptyMetadataAsEmptyJsonObject() {
var payment = anInitiatedPayment();
var saved = adapter.save(payment);

var found = adapter.findById(saved.paymentId());
assertThat(found).isPresent();
assertThat(found.get().metadata()).isEmpty();
}

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 | 🟠 Major

This does not verify that {} was persisted.

The current check only proves the adapter returns an empty map on read. It still passes if the column is stored as NULL and normalized during mapping. If the contract is “empty JSON object”, assert the raw persisted value, or at least the entity field before domain mapping, is non-null and empty.

As per coding guidelines, "Assert on meaningful values — avoid assertTrue(result != null)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`
around lines 166 - 175, The test should assert the actual persisted JSON column,
not just the mapped domain metadata; after saving via adapter.save(payment) (use
saved.paymentId()), query the persistence side directly (e.g., via JdbcTemplate
or the PaymentEntity repository/finder) to fetch the raw metadata column for
that payment id and assert it is non-null and equals "{}" (or otherwise the
exact expected empty JSON representation) instead of only asserting
found.get().metadata() isEmpty(); update
shouldPersistEmptyMetadataAsEmptyJsonObject to perform that direct DB assertion
using the payment id returned by adapter.save.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java (3)

121-123: ⚠️ Potential issue | 🟡 Minor

Assert the exact rows returned by findBySenderIdAndState.

A size-only check will miss state-filtering or mapping regressions as long as two rows come back. Assert the returned IDs and PaymentState values, order-agnostic.

Suggested assertion tightening
         var results = adapter.findBySenderIdAndState(SENDER_ID, PaymentState.INITIATED);
-        assertThat(results).hasSize(2);
+        assertThat(results)
+                .extracting(Payment::paymentId)
+                .containsExactlyInAnyOrder(payment1.paymentId(), payment2.paymentId());
+        assertThat(results)
+                .allMatch(payment -> payment.state() == PaymentState.INITIATED);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`
around lines 121 - 123, Replace the weak size-only assertion on the results from
adapter.findBySenderIdAndState(SENDER_ID, PaymentState.INITIATED) with an
order-agnostic assertion that the exact rows are returned: assert that the
collection contains the expected payment IDs and that each returned Payment has
PaymentState.INITIATED (e.g., by extracting id and state from results and
comparing as sets or comparing a Set of ids and verifying all states equal
PaymentState.INITIATED), referencing the results variable and the
adapter.findBySenderIdAndState call to locate the code.

60-64: ⚠️ Potential issue | 🟡 Minor

Stop excluding all Instant fields from the round-trip assertions.

These assertions still pass if createdAt, updatedAt, or expiresAt are truncated, shifted, or dropped. Compare those fields explicitly at DB precision instead of ignoringFieldsOfTypes(Instant.class).

Also applies to: 73-77, 94-98, 240-244, 259-263, 300-304

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`
around lines 60 - 64, The assertion currently ignores all Instant fields in the
round-trip check (the adapter.findById(...).isPresent().get() assertion chain),
which hides DB-level truncation or loss of createdAt/updatedAt/expiresAt; remove
ignoringFieldsOfTypes(Instant.class) and instead assert those Instant fields
explicitly by comparing them at DB precision (e.g., truncate or round both saved
and returned createdAt/updatedAt/expiresAt to the database precision) within the
same recursive comparison or by separate assertions; apply the same change to
the other occurrences mentioned (the similar chains at the other ranges) so each
Instant field is compared deterministically rather than excluded.

168-176: ⚠️ Potential issue | 🟠 Major

Verify the raw JSONB value for empty metadata.

This only proves the mapper returns an empty map. It still passes if the column is stored as NULL and normalized on read. Please assert the persisted metadata column or entity field is non-null and represents an empty JSON object for the saved row.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`
around lines 168 - 176, The test currently only checks the returned object's
metadata map; instead assert the persisted raw JSONB column is non-null and
equals an empty JSON object: after calling adapter.save(payment) (in
PaymentPersistenceAdapterIT) flush or use the same persistence layer (e.g.,
entityManager, jdbcTemplate, or repository) to query the saved row for
saved.paymentId() and read the metadata column value directly, then assert the
column is not NULL and its string/value equals "{}" (or an empty JSON object) in
addition to the existing mapper assertion. Use the concrete persistence accessor
available in the test (entityManager/ jdbcTemplate/ repository) to fetch the raw
column for verification.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`:
- Around line 181-205: The test currently exercises JPA directly instead of the
adapter, so update it to reproduce the optimistic-lock conflict through the
adapter: build a stale domain aggregate (use mapper.toEntity or create a domain
Payment with the old version), set its version to staleVersion and a new state
(FAILED), clear the EntityManager, then call
PaymentPersistenceAdapter.save(staleAggregate) (adapter.save(...)) and assert
that it throws ObjectOptimisticLockingFailureException; this ensures the broken
path in PaymentPersistenceAdapter.save (which currently reloads the entity
before saving) is tested via the adapter rather than via
PaymentJpaRepository.saveAndFlush.

---

Duplicate comments:
In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`:
- Around line 121-123: Replace the weak size-only assertion on the results from
adapter.findBySenderIdAndState(SENDER_ID, PaymentState.INITIATED) with an
order-agnostic assertion that the exact rows are returned: assert that the
collection contains the expected payment IDs and that each returned Payment has
PaymentState.INITIATED (e.g., by extracting id and state from results and
comparing as sets or comparing a Set of ids and verifying all states equal
PaymentState.INITIATED), referencing the results variable and the
adapter.findBySenderIdAndState call to locate the code.
- Around line 60-64: The assertion currently ignores all Instant fields in the
round-trip check (the adapter.findById(...).isPresent().get() assertion chain),
which hides DB-level truncation or loss of createdAt/updatedAt/expiresAt; remove
ignoringFieldsOfTypes(Instant.class) and instead assert those Instant fields
explicitly by comparing them at DB precision (e.g., truncate or round both saved
and returned createdAt/updatedAt/expiresAt to the database precision) within the
same recursive comparison or by separate assertions; apply the same change to
the other occurrences mentioned (the similar chains at the other ranges) so each
Instant field is compared deterministically rather than excluded.
- Around line 168-176: The test currently only checks the returned object's
metadata map; instead assert the persisted raw JSONB column is non-null and
equals an empty JSON object: after calling adapter.save(payment) (in
PaymentPersistenceAdapterIT) flush or use the same persistence layer (e.g.,
entityManager, jdbcTemplate, or repository) to query the saved row for
saved.paymentId() and read the metadata column value directly, then assert the
column is not NULL and its string/value equals "{}" (or an empty JSON object) in
addition to the existing mapper assertion. Use the concrete persistence accessor
available in the test (entityManager/ jdbcTemplate/ repository) to fetch the raw
column for verification.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: cbad2531-bae4-4232-9e7e-9961cdc21f6a

📥 Commits

Reviewing files that changed from the base of the PR and between 17e4835 and f75d471.

📒 Files selected for processing (3)
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java
  • payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/domain/port/PaymentRepository.java
  • payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapter.java

Comment on lines +181 to +205
@Test
@DisplayName("should detect optimistic locking conflict on concurrent update")
void shouldDetectOptimisticLockingConflict() {
var payment = anInitiatedPayment();
adapter.save(payment);

// Load entity and detach to simulate a separate session
var entity1 = jpaRepository.findById(payment.paymentId()).orElseThrow();
var staleVersion = entity1.getVersion();

// First update via adapter increments the version
var transitioned = payment.startComplianceCheck();
adapter.save(transitioned);

// Build a stale entity with the old version to simulate concurrent access
var staleEntity = mapper.toEntity(payment);
staleEntity.setVersion(staleVersion);
staleEntity.setState(PaymentState.FAILED);
staleEntity.setUpdatedAt(Instant.now());

// Clear persistence context so Hibernate treats staleEntity as detached
entityManager.clear();

assertThatThrownBy(() -> jpaRepository.saveAndFlush(staleEntity))
.isInstanceOf(ObjectOptimisticLockingFailureException.class);

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 | 🔴 Critical

Exercise optimistic locking through PaymentPersistenceAdapter.

This only proves PaymentJpaRepository honors @Version. Line 27-33 in payment-orchestrator/payment-orchestrator/src/main/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapter.java reloads the current entity before saving, so a stale aggregate can overwrite newer state without surfacing ObjectOptimisticLockingFailureException. Please reproduce the conflict via adapter.save(...); otherwise the lost-update path in the port remains untested.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`
around lines 181 - 205, The test currently exercises JPA directly instead of the
adapter, so update it to reproduce the optimistic-lock conflict through the
adapter: build a stale domain aggregate (use mapper.toEntity or create a domain
Payment with the old version), set its version to staleVersion and a new state
(FAILED), clear the EntityManager, then call
PaymentPersistenceAdapter.save(staleAggregate) (adapter.save(...)) and assert
that it throws ObjectOptimisticLockingFailureException; this ensures the broken
path in PaymentPersistenceAdapter.save (which currently reloads the entity
before saving) is tested via the adapter rather than via
PaymentJpaRepository.saveAndFlush.

Puneethkumarck and others added 4 commits March 8, 2026 09:12
…(STA-108)

- PaymentPersistenceAdapterIT: 13 tests covering save/retrieve round-trip,
  findByIdempotencyKey, findBySenderIdAndState, JSONB metadata, optimistic
  locking conflict, unique constraint violation, and state transition updates
- PaymentAuditLogRepositoryIT: 7 tests covering append-only insert,
  query by payment_id ordered by occurred_at desc, JSONB metadata round-trip
- V3 migration: fix version column type INT -> BIGINT (Hibernate maps Long)
- TestKafkaConfig: typed KafkaTemplate<String, Object> for outbox handler
- AbstractIntegrationTest: @beforeeach database cleanup + entityManager.clear()
- application-integration-test.yml: full JPA, Flyway, Kafka, namastack config

Closes STA-108

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ndState, full-path test (STA-108)

- Add findBySenderIdAndState to PaymentRepository port and implement in adapter
  so tests go through the adapter layer, not bypass to JPA directly
- Rewrite shouldUpdatePaymentThroughFullHappyPath to walk through all state
  transitions from INITIATED to COMPLETED instead of inserting a pre-built fixture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace scattered metadata assertThat calls with single
usingRecursiveComparison().isEqualTo(expected) for JSONB tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck force-pushed the feature/STA-108-s1-persistence-tests branch from 28b6eef to 5544d0e Compare March 8, 2026 08:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java (1)

121-123: ⚠️ Potential issue | 🟡 Minor

Strengthen assertions on returned payments.

The test correctly routes through the adapter now, but hasSize(2) doesn't verify these are the exact payments you saved or that their states are INITIATED. Extract the saved payment IDs and assert the results contain them with the expected state.

💡 Proposed improvement
+        var saved1 = adapter.save(payment1);
+        var saved2 = adapter.save(payment2);
-        adapter.save(payment1);
-        adapter.save(payment2);

         var results = adapter.findBySenderIdAndState(SENDER_ID, PaymentState.INITIATED);
-        assertThat(results).hasSize(2);
+        assertThat(results)
+                .hasSize(2)
+                .extracting(Payment::paymentId)
+                .containsExactlyInAnyOrder(saved1.paymentId(), saved2.paymentId());
+        assertThat(results).allMatch(p -> p.state() == PaymentState.INITIATED);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`
around lines 121 - 123, The test currently only checks size; update it to assert
that the returned payments from adapter.findBySenderIdAndState(SENDER_ID,
PaymentState.INITIATED) include the specific saved payment IDs and that each
returned payment has state PaymentState.INITIATED. Locate the saved payment
fixtures/variables used earlier in the test (e.g., the saved payment objects or
their IDs), extract their IDs, and replace the loose hasSize(2) with assertions
that results containsExactlyInAnyOrder/their IDs (or contains IDs) and that
results.stream().allMatch(p -> p.getState() == PaymentState.INITIATED) to verify
states.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`:
- Around line 268-307: The test currently saves each domain object but continues
to call subsequent transition methods on the original in-memory objects (e.g.,
payment.startComplianceCheck(), complianceCheck.lockFxRate(...),
fxLocked.startFiatCollection(), etc.), which misses verifying the persisted
round-tripped state; fix this by chaining each transition off the object
returned from adapter.save (assign the result of adapter.save(...) to a variable
and call the next transition on that saved object for every step through
lockFxRate, startFiatCollection, confirmFiatCollected, submitOnChain,
confirmOnChain, initiateOffRamp, settle, complete) so the test exercises state
after persistence before asserting via adapter.findById(...).
- Around line 251-266: The test discards adapter.save() return values causing
state drift; change the flow to chain off saved entities by assigning the
results of adapter.save(...) before calling transitions: call and assign payment
= adapter.save(payment) before payment.startComplianceCheck(), assign
complianceCheck = adapter.save(complianceCheck) before
complianceCheck.lockFxRate(...), and assign fxLocked = adapter.save(fxLocked)
before asserting via adapter.findById(payment.paymentId()); this ensures any
modified fields (versions, timestamps, IDs) returned by save are used for
subsequent transitions and comparisons.

---

Duplicate comments:
In
`@payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java`:
- Around line 121-123: The test currently only checks size; update it to assert
that the returned payments from adapter.findBySenderIdAndState(SENDER_ID,
PaymentState.INITIATED) include the specific saved payment IDs and that each
returned payment has state PaymentState.INITIATED. Locate the saved payment
fixtures/variables used earlier in the test (e.g., the saved payment objects or
their IDs), extract their IDs, and replace the loose hasSize(2) with assertions
that results containsExactlyInAnyOrder/their IDs (or contains IDs) and that
results.stream().allMatch(p -> p.getState() == PaymentState.INITIATED) to verify
states.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 305c0621-b7b7-4c68-9c8f-e60129b1adc2

📥 Commits

Reviewing files that changed from the base of the PR and between f75d471 and 28b6eef.

📒 Files selected for processing (1)
  • payment-orchestrator/payment-orchestrator/src/integration-test/java/com/stablecoin/payments/orchestrator/infrastructure/persistence/PaymentPersistenceAdapterIT.java

@Puneethkumarck Puneethkumarck merged commit 50b4c56 into main Mar 8, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request phase-2 Core Payment Logic

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant