Skip to content

feat(s5): domain model unit tests — 149 tests for PayoutOrder SM, VOs, entities (STA-148)#153

Merged
Puneethkumarck merged 1 commit into
mainfrom
feature/STA-148-s5-domain-unit-tests
Mar 9, 2026
Merged

feat(s5): domain model unit tests — 149 tests for PayoutOrder SM, VOs, entities (STA-148)#153
Puneethkumarck merged 1 commit into
mainfrom
feature/STA-148-s5-domain-unit-tests

Conversation

@Puneethkumarck

@Puneethkumarck Puneethkumarck commented Mar 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • 149 new domain model unit tests for S5 Fiat Off-Ramp service (157 total with 8 ArchUnit)
  • PayoutOrder state machine: all 12 transitions, fiat amount tolerance invariant (±0.01), terminal state guards, canApply() queries, immutability assertions
  • Value objects: BankAccount, MobileMoneyAccount, Money, StablecoinTicker (5 supported tickers), PartnerIdentifier
  • Entities: StablecoinRedemption, OffRampTransaction creation + validation
  • PayoutOrderFixtures in testFixtures with state factories for all 10 PayoutOrder states

Test Breakdown

Test File Tests Coverage
PayoutOrderTest 82 SM transitions, tolerance, terminal guards, canApply, validation
StablecoinRedemptionTest 13 Creation, validation
OffRampTransactionTest 11 Creation, default rawResponse, validation
StablecoinTickerTest 13 All 5 tickers, unsupported, auto-fill, explicit constructor
BankAccountTest 9 All 4 field validations
MobileMoneyAccountTest 6 All 3 field validations
MoneyTest 6 Amount + currency validation
PartnerIdentifierTest 5 Both field validations
ArchitectureTest 8 Hexagonal architecture rules
Total 157

Test plan

  • All 157 tests pass (./gradlew :fiat-off-ramp:fiat-off-ramp:test)
  • Spotless formatting clean
  • Single-assert pattern followed throughout
  • Fixtures in testFixtures (not private methods in test classes)
  • CI green

Closes STA-148

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Tests
    • Expanded test coverage for domain models with comprehensive unit test suites validating constructor behavior, validation rules, and state transitions.
    • Added test fixtures to support consistent test data creation across test suites.

…, entities (STA-148)

Test coverage for all S5 domain model components:
- PayoutOrder: 12 state machine transitions (happy path, hold path, failure paths),
  fiat amount tolerance invariant (±0.01), terminal state guards, canApply() queries,
  immutability, factory validation (20 fields)
- StablecoinRedemption: creation + 10 validation cases
- OffRampTransaction: creation + default rawResponse + 6 validation cases
- Value objects: BankAccount (8), MobileMoneyAccount (6), Money (6),
  StablecoinTicker (12 — all 5 supported tickers, unsupported, auto-fill),
  PartnerIdentifier (5)
- PayoutOrderFixtures in testFixtures with state factories for all 10 states

157 total tests (149 new + 8 ArchUnit). All green.

Closes STA-148

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Puneethkumarck Puneethkumarck added phase-3 Value Movement MVP service-s5 Off-Ramp test Test coverage labels Mar 9, 2026
@coderabbitai

coderabbitai Bot commented Mar 9, 2026

Copy link
Copy Markdown

Walkthrough

Comprehensive unit test suite added for fiat off-ramp domain models, covering validation, state transitions, and immutability. Includes tests for value objects (BankAccount, Money, StablecoinTicker), transaction entities, and a state machine (PayoutOrder). Fixture utility provided for test data construction.

Changes

Cohort / File(s) Summary
Value Object Tests
BankAccountTest.java, MobileMoneyAccountTest.java, MoneyTest.java, StablecoinTickerTest.java, PartnerIdentifierTest.java
Unit tests for constructor validation and factory methods. Covers null/blank/zero/negative input rejection with proper exception messages, recursive equality assertions, and issuer/decimal defaulting logic (StablecoinTicker).
Transaction Domain Tests
OffRampTransactionTest.java, StablecoinRedemptionTest.java
Tests for creation, field population, timestamp semantics, default values (raw response), and validation of required fields (payoutId, partner references, amounts, currencies). Negative tests verify exception messages reference specific field names.
State Machine Tests
PayoutOrderTest.java
Extensive coverage of PayoutOrder state transitions (Fiat path: PENDING → REDEMPTION → PAYOUT → COMPLETED; Hold path: PENDING → HOLD → COMPLETED_HOLD). Validates terminal state guards, invalid transitions throw StateMachineException, fiat amount tolerance (±0.01), immutability across transitions, and canApply() query coverage for 13+ state and trigger combinations.
Test Fixtures
PayoutOrderFixtures.java
Public utility factory methods for composing domain value objects (BankAccount, MobileMoneyAccount, StablecoinTicker, Money) and PayoutOrder instances in various lifecycle states (pending, redeeming, redeemed, payout initiated/processing/completed/failed, manual review, stablecoin held). Enables fluent test data construction via state transition chains.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

feature

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.41% 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 accurately summarizes the main change—149 domain model unit tests for PayoutOrder state machine and value objects—and includes the ticket reference (STA-148).
Description check ✅ Passed Description covers summary, test breakdown, test plan status, and related issue. All major template sections present except optional Breaking Changes and partially Security Considerations.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/STA-148-s5-domain-unit-tests

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

@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
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/OffRampTransactionTest.java`:
- Around line 53-71: The tests for OffRampTransaction should be made
deterministic: when calling OffRampTransaction.create(...) assert that
offRampTxnId() is not only non-null but different between two separately created
instances (create two txns and assert txn1.offRampTxnId() !=
txn2.offRampTxnId()), and tighten the receivedAt() check by capturing two
timestamps (Instant before = Instant.now(); create txn; Instant after =
Instant.now()) and assert txn.receivedAt() isBetween(before, after) (or assert
receivedAt() isAfterOrEqualTo(before) and isBeforeOrEqualTo(after)) to ensure
timestamps are bounded; update the test methods that call
OffRampTransaction.create, offRampTxnId(), and receivedAt() accordingly.

In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/PayoutOrderTest.java`:
- Around line 657-662: Update the test manualReviewRejectsEscalate in
PayoutOrderTest to also assert the exception message contains the expected
terminal text: when calling aManualReviewOrder().escalateToManualReview()
capture the StateMachineException and add hasMessageContaining("terminal") (or
the same message used by other terminal-state tests) so the test verifies both
exception type and consistent message contract for terminal-state guards.
- Around line 268-281: Add a new unit test in PayoutOrderTest that mirrors
throwsWhenAppliedFxRateZero but passes a negative BigDecimal for appliedFxRate
to PayoutOrder.create; assert it throws IllegalArgumentException and
hasMessageContaining("appliedFxRate"). Locate the test class PayoutOrderTest and
add a test method (e.g., throwsWhenAppliedFxRateNegative) calling
PayoutOrder.create with BigDecimal.valueOf(-1) (or an equivalent negative value)
for appliedFxRate to cover the <= 0 validation in PayoutOrder.create.

In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/StablecoinRedemptionTest.java`:
- Around line 73-82: The test populatesStablecoinTicker currently claims to
verify issuer and decimals but only asserts ticker and issuer; either add an
assertion for the decimals (e.g.,
assertThat(redemption.stablecoin().decimals()).isEqualTo(expectedDecimals))
after creating the StablecoinRedemption via
StablecoinRedemption.create(PAYOUT_ID, aStablecoinTicker(), ...) or change the
`@DisplayName` to remove "and decimals" so it reflects only ticker and issuer;
update the test method populatesStablecoinTicker accordingly and use the
stablecoin() accessor (ticker(), issuer(), decimals()) to make the chosen
change.
- Around line 52-71: The tests use weak assertions: improve the factory
assertions in StablecoinRedemptionTest by creating two independent instances via
StablecoinRedemption.create and assert their redemptionId() values are non-null
and not equal to each other (to rule out a constant), and for redeemedAt()
capture an Instant before and after creation and assert redeemedAt() is >=
before and <= after to prevent future timestamps; update the tests referencing
StablecoinRedemption.create, redemptionId(), and redeemedAt() accordingly.

In
`@fiat-off-ramp/fiat-off-ramp/src/testFixtures/java/com/stablecoin/payments/offramp/fixtures/PayoutOrderFixtures.java`:
- Around line 115-117: The aCompletedOrder() fixture is non-deterministic due to
Instant.now(); change it to use a fixed Instant constant (e.g., COMPLETED_AT) or
make the method accept an Instant parameter with a default constant to ensure
deterministic returns. Update aCompletedOrder() to call
aPayoutProcessingOrder().completePayout(PARTNER_REFERENCE, COMPLETED_AT) (or
delegate to a new overloaded aCompletedOrder(Instant completedAt)) and add the
COMPLETED_AT constant in the fixture class so tests get stable, comparable
objects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 49569095-9491-4b15-ae93-3f23e54d7bfd

📥 Commits

Reviewing files that changed from the base of the PR and between 0ba932b and 80bc587.

📒 Files selected for processing (9)
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/BankAccountTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/MobileMoneyAccountTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/MoneyTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/OffRampTransactionTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/PartnerIdentifierTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/PayoutOrderTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/StablecoinRedemptionTest.java
  • fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/StablecoinTickerTest.java
  • fiat-off-ramp/fiat-off-ramp/src/testFixtures/java/com/stablecoin/payments/offramp/fixtures/PayoutOrderFixtures.java

Comment on lines +53 to +71
@Test
@DisplayName("generates a random offRampTxnId")
void generatesRandomTxnId() {
var txn = OffRampTransaction.create(
PAYOUT_ID, PARTNER_NAME, EVENT_TYPE,
AMOUNT, CURRENCY, STATUS, RAW_RESPONSE);

assertThat(txn.offRampTxnId()).isNotNull();
}

@Test
@DisplayName("sets receivedAt timestamp")
void setsReceivedAt() {
var before = Instant.now();
var txn = OffRampTransaction.create(
PAYOUT_ID, PARTNER_NAME, EVENT_TYPE,
AMOUNT, CURRENCY, STATUS, RAW_RESPONSE);

assertThat(txn.receivedAt()).isAfterOrEqualTo(before);

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

Make the generated ID and timestamp checks deterministic enough to catch regressions.

Right now these tests only prove “non-null” and “not before before”. A fixed offRampTxnId or a future receivedAt would still pass. Compare two creations for different IDs and bound receivedAt with an after timestamp.

Suggested test hardening
     `@Test`
     `@DisplayName`("generates a random offRampTxnId")
     void generatesRandomTxnId() {
-        var txn = OffRampTransaction.create(
+        var first = OffRampTransaction.create(
                 PAYOUT_ID, PARTNER_NAME, EVENT_TYPE,
                 AMOUNT, CURRENCY, STATUS, RAW_RESPONSE);
+        var second = OffRampTransaction.create(
+                PAYOUT_ID, PARTNER_NAME, EVENT_TYPE,
+                AMOUNT, CURRENCY, STATUS, RAW_RESPONSE);
 
-        assertThat(txn.offRampTxnId()).isNotNull();
+        assertThat(first.offRampTxnId()).isNotNull();
+        assertThat(first.offRampTxnId()).isNotEqualTo(second.offRampTxnId());
     }
 
     `@Test`
     `@DisplayName`("sets receivedAt timestamp")
     void setsReceivedAt() {
         var before = Instant.now();
         var txn = OffRampTransaction.create(
                 PAYOUT_ID, PARTNER_NAME, EVENT_TYPE,
                 AMOUNT, CURRENCY, STATUS, RAW_RESPONSE);
+        var after = Instant.now();
 
-        assertThat(txn.receivedAt()).isAfterOrEqualTo(before);
+        assertThat(txn.receivedAt())
+                .isAfterOrEqualTo(before)
+                .isBeforeOrEqualTo(after);
     }

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
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/OffRampTransactionTest.java`
around lines 53 - 71, The tests for OffRampTransaction should be made
deterministic: when calling OffRampTransaction.create(...) assert that
offRampTxnId() is not only non-null but different between two separately created
instances (create two txns and assert txn1.offRampTxnId() !=
txn2.offRampTxnId()), and tighten the receivedAt() check by capturing two
timestamps (Instant before = Instant.now(); create txn; Instant after =
Instant.now()) and assert txn.receivedAt() isBetween(before, after) (or assert
receivedAt() isAfterOrEqualTo(before) and isBeforeOrEqualTo(after)) to ensure
timestamps are bounded; update the test methods that call
OffRampTransaction.create, offRampTxnId(), and receivedAt() accordingly.

Comment on lines +268 to +281
@Test
@DisplayName("throws when appliedFxRate is zero")
void throwsWhenAppliedFxRateZero() {
assertThatThrownBy(() -> PayoutOrder.create(
PAYMENT_ID, CORRELATION_ID, TRANSFER_ID,
PayoutType.FIAT, aStablecoinTicker(),
REDEEMED_AMOUNT, TARGET_CURRENCY,
BigDecimal.ZERO, RECIPIENT_ID,
RECIPIENT_ACCOUNT_HASH,
aBankAccount(), null,
PaymentRail.SEPA, aPartnerIdentifier()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("appliedFxRate");
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if PayoutOrder.create() validates negative appliedFxRate
ast-grep --pattern $'if ($_ <= 0) {
  $$$
}'

# Also search for appliedFxRate validation logic
rg -n "appliedFxRate" --type java -A3 -B1 | head -60

Repository: Puneethkumarck/stablebridge-platform

Length of output: 21971


Add test case for negative appliedFxRate.

The domain validates that appliedFxRate must be positive (line 135 of PayoutOrder.java rejects <= 0). Tests exist for null and zero cases, but a negative-value test is missing for complete coverage.

Suggested test case
+        `@Test`
+        `@DisplayName`("throws when appliedFxRate is negative")
+        void throwsWhenAppliedFxRateNegative() {
+            assertThatThrownBy(() -> PayoutOrder.create(
+                    PAYMENT_ID, CORRELATION_ID, TRANSFER_ID,
+                    PayoutType.FIAT, aStablecoinTicker(),
+                    REDEEMED_AMOUNT, TARGET_CURRENCY,
+                    new BigDecimal("-0.92"), RECIPIENT_ID,
+                    RECIPIENT_ACCOUNT_HASH,
+                    aBankAccount(), null,
+                    PaymentRail.SEPA, aPartnerIdentifier()))
+                    .isInstanceOf(IllegalArgumentException.class)
+                    .hasMessageContaining("appliedFxRate");
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
@DisplayName("throws when appliedFxRate is zero")
void throwsWhenAppliedFxRateZero() {
assertThatThrownBy(() -> PayoutOrder.create(
PAYMENT_ID, CORRELATION_ID, TRANSFER_ID,
PayoutType.FIAT, aStablecoinTicker(),
REDEEMED_AMOUNT, TARGET_CURRENCY,
BigDecimal.ZERO, RECIPIENT_ID,
RECIPIENT_ACCOUNT_HASH,
aBankAccount(), null,
PaymentRail.SEPA, aPartnerIdentifier()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("appliedFxRate");
}
`@Test`
`@DisplayName`("throws when appliedFxRate is zero")
void throwsWhenAppliedFxRateZero() {
assertThatThrownBy(() -> PayoutOrder.create(
PAYMENT_ID, CORRELATION_ID, TRANSFER_ID,
PayoutType.FIAT, aStablecoinTicker(),
REDEEMED_AMOUNT, TARGET_CURRENCY,
BigDecimal.ZERO, RECIPIENT_ID,
RECIPIENT_ACCOUNT_HASH,
aBankAccount(), null,
PaymentRail.SEPA, aPartnerIdentifier()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("appliedFxRate");
}
`@Test`
`@DisplayName`("throws when appliedFxRate is negative")
void throwsWhenAppliedFxRateNegative() {
assertThatThrownBy(() -> PayoutOrder.create(
PAYMENT_ID, CORRELATION_ID, TRANSFER_ID,
PayoutType.FIAT, aStablecoinTicker(),
REDEEMED_AMOUNT, TARGET_CURRENCY,
new BigDecimal("-0.92"), RECIPIENT_ID,
RECIPIENT_ACCOUNT_HASH,
aBankAccount(), null,
PaymentRail.SEPA, aPartnerIdentifier()))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("appliedFxRate");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/PayoutOrderTest.java`
around lines 268 - 281, Add a new unit test in PayoutOrderTest that mirrors
throwsWhenAppliedFxRateZero but passes a negative BigDecimal for appliedFxRate
to PayoutOrder.create; assert it throws IllegalArgumentException and
hasMessageContaining("appliedFxRate"). Locate the test class PayoutOrderTest and
add a test method (e.g., throwsWhenAppliedFxRateNegative) calling
PayoutOrder.create with BigDecimal.valueOf(-1) (or an equivalent negative value)
for appliedFxRate to cover the <= 0 validation in PayoutOrder.create.

Comment on lines +657 to +662
@Test
@DisplayName("MANUAL_REVIEW order rejects escalateToManualReview")
void manualReviewRejectsEscalate() {
assertThatThrownBy(() -> aManualReviewOrder().escalateToManualReview())
.isInstanceOf(StateMachineException.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.

🧹 Nitpick | 🔵 Trivial

Consider asserting on exception message for consistency.

Other terminal state guard tests assert hasMessageContaining("terminal"), but this test only checks exception type. If escalateToManualReview on MANUAL_REVIEW throws StateMachineException (rather than IllegalStateException), it suggests different validation logic—valid, but documenting the expected message would strengthen the contract test.

💡 Optional consistency improvement
         `@Test`
         `@DisplayName`("MANUAL_REVIEW order rejects escalateToManualReview")
         void manualReviewRejectsEscalate() {
             assertThatThrownBy(() -> aManualReviewOrder().escalateToManualReview())
-                    .isInstanceOf(StateMachineException.class);
+                    .isInstanceOf(StateMachineException.class)
+                    .hasMessageContaining("MANUAL_REVIEW"); // or relevant message
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/PayoutOrderTest.java`
around lines 657 - 662, Update the test manualReviewRejectsEscalate in
PayoutOrderTest to also assert the exception message contains the expected
terminal text: when calling aManualReviewOrder().escalateToManualReview()
capture the StateMachineException and add hasMessageContaining("terminal") (or
the same message used by other terminal-state tests) so the test verifies both
exception type and consistent message contract for terminal-state guards.

Comment on lines +52 to +71
@Test
@DisplayName("generates a random redemptionId")
void generatesRandomRedemptionId() {
var redemption = StablecoinRedemption.create(
PAYOUT_ID, aStablecoinTicker(), REDEEMED_AMOUNT,
FIAT_RECEIVED, FIAT_CURRENCY, PARTNER, PARTNER_REFERENCE);

assertThat(redemption.redemptionId()).isNotNull();
}

@Test
@DisplayName("sets redeemedAt timestamp")
void setsRedeemedAt() {
var before = Instant.now();
var redemption = StablecoinRedemption.create(
PAYOUT_ID, aStablecoinTicker(), REDEEMED_AMOUNT,
FIAT_RECEIVED, FIAT_CURRENCY, PARTNER, PARTNER_REFERENCE);

assertThat(redemption.redeemedAt()).isAfterOrEqualTo(before);
}

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

Strengthen the generated-field assertions.

These checks are too weak for factory-generated values: isNotNull() will not catch a constant redemptionId, and the one-sided redeemedAt assertion will still pass for future timestamps. Create two instances and bound the timestamp between before and after.

Suggested test hardening
     `@Test`
     `@DisplayName`("generates a random redemptionId")
     void generatesRandomRedemptionId() {
-        var redemption = StablecoinRedemption.create(
+        var first = StablecoinRedemption.create(
                 PAYOUT_ID, aStablecoinTicker(), REDEEMED_AMOUNT,
                 FIAT_RECEIVED, FIAT_CURRENCY, PARTNER, PARTNER_REFERENCE);
+        var second = StablecoinRedemption.create(
+                PAYOUT_ID, aStablecoinTicker(), REDEEMED_AMOUNT,
+                FIAT_RECEIVED, FIAT_CURRENCY, PARTNER, PARTNER_REFERENCE);
 
-        assertThat(redemption.redemptionId()).isNotNull();
+        assertThat(first.redemptionId()).isNotNull();
+        assertThat(first.redemptionId()).isNotEqualTo(second.redemptionId());
     }
 
     `@Test`
     `@DisplayName`("sets redeemedAt timestamp")
     void setsRedeemedAt() {
         var before = Instant.now();
         var redemption = StablecoinRedemption.create(
                 PAYOUT_ID, aStablecoinTicker(), REDEEMED_AMOUNT,
                 FIAT_RECEIVED, FIAT_CURRENCY, PARTNER, PARTNER_REFERENCE);
+        var after = Instant.now();
 
-        assertThat(redemption.redeemedAt()).isAfterOrEqualTo(before);
+        assertThat(redemption.redeemedAt())
+                .isAfterOrEqualTo(before)
+                .isBeforeOrEqualTo(after);
     }

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
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/StablecoinRedemptionTest.java`
around lines 52 - 71, The tests use weak assertions: improve the factory
assertions in StablecoinRedemptionTest by creating two independent instances via
StablecoinRedemption.create and assert their redemptionId() values are non-null
and not equal to each other (to rule out a constant), and for redeemedAt()
capture an Instant before and after creation and assert redeemedAt() is >=
before and <= after to prevent future timestamps; update the tests referencing
StablecoinRedemption.create, redemptionId(), and redeemedAt() accordingly.

Comment on lines +73 to +82
@Test
@DisplayName("populates stablecoin ticker with issuer and decimals")
void populatesStablecoinTicker() {
var redemption = StablecoinRedemption.create(
PAYOUT_ID, aStablecoinTicker(), REDEEMED_AMOUNT,
FIAT_RECEIVED, FIAT_CURRENCY, PARTNER, PARTNER_REFERENCE);

assertThat(redemption.stablecoin().ticker()).isEqualTo("USDC");
assertThat(redemption.stablecoin().issuer()).isEqualTo("circle");
}

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 decimals or rename the test.

Line 74 says this verifies issuer and decimals, but the body only checks ticker and issuer. Add a decimals assertion or narrow the display name so the spec matches the behavior.

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

In
`@fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/StablecoinRedemptionTest.java`
around lines 73 - 82, The test populatesStablecoinTicker currently claims to
verify issuer and decimals but only asserts ticker and issuer; either add an
assertion for the decimals (e.g.,
assertThat(redemption.stablecoin().decimals()).isEqualTo(expectedDecimals))
after creating the StablecoinRedemption via
StablecoinRedemption.create(PAYOUT_ID, aStablecoinTicker(), ...) or change the
`@DisplayName` to remove "and decimals" so it reflects only ticker and issuer;
update the test method populatesStablecoinTicker accordingly and use the
stablecoin() accessor (ticker(), issuer(), decimals()) to make the chosen
change.

Comment on lines +115 to +117
public static PayoutOrder aCompletedOrder() {
return aPayoutProcessingOrder().completePayout(PARTNER_REFERENCE, java.time.Instant.now());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Remove the wall-clock dependency from the shared completed-order fixture.

aCompletedOrder() currently returns a different object on every call because of Instant.now(). That makes fixture-based equality and immutability assertions harder to trust. Use a fixed timestamp constant, or accept the completion time as a parameter and keep the default deterministic.

Suggested fixture change
+    public static final java.time.Instant COMPLETED_AT =
+            java.time.Instant.parse("2026-01-01T00:00:00Z");
+
     public static PayoutOrder aCompletedOrder() {
-        return aPayoutProcessingOrder().completePayout(PARTNER_REFERENCE, java.time.Instant.now());
+        return aPayoutProcessingOrder().completePayout(PARTNER_REFERENCE, COMPLETED_AT);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@fiat-off-ramp/fiat-off-ramp/src/testFixtures/java/com/stablecoin/payments/offramp/fixtures/PayoutOrderFixtures.java`
around lines 115 - 117, The aCompletedOrder() fixture is non-deterministic due
to Instant.now(); change it to use a fixed Instant constant (e.g., COMPLETED_AT)
or make the method accept an Instant parameter with a default constant to ensure
deterministic returns. Update aCompletedOrder() to call
aPayoutProcessingOrder().completePayout(PARTNER_REFERENCE, COMPLETED_AT) (or
delegate to a new overloaded aCompletedOrder(Instant completedAt)) and add the
COMPLETED_AT constant in the fixture class so tests get stable, comparable
objects.

@Puneethkumarck Puneethkumarck merged commit f0b533a into main Mar 9, 2026
18 of 19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

phase-3 Value Movement MVP service-s5 Off-Ramp test Test coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant