feat(s5): domain model unit tests — 149 tests for PayoutOrder SM, VOs, entities (STA-148)#153
Conversation
…, 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>
WalkthroughComprehensive 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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (9)
fiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/BankAccountTest.javafiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/MobileMoneyAccountTest.javafiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/MoneyTest.javafiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/OffRampTransactionTest.javafiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/PartnerIdentifierTest.javafiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/PayoutOrderTest.javafiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/StablecoinRedemptionTest.javafiat-off-ramp/fiat-off-ramp/src/test/java/com/stablecoin/payments/offramp/domain/model/StablecoinTickerTest.javafiat-off-ramp/fiat-off-ramp/src/testFixtures/java/com/stablecoin/payments/offramp/fixtures/PayoutOrderFixtures.java
| @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); |
There was a problem hiding this comment.
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.
| @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"); | ||
| } |
There was a problem hiding this comment.
🧹 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 -60Repository: 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.
| @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.
| @Test | ||
| @DisplayName("MANUAL_REVIEW order rejects escalateToManualReview") | ||
| void manualReviewRejectsEscalate() { | ||
| assertThatThrownBy(() -> aManualReviewOrder().escalateToManualReview()) | ||
| .isInstanceOf(StateMachineException.class); | ||
| } |
There was a problem hiding this comment.
🧹 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.
| @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); | ||
| } |
There was a problem hiding this comment.
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.
| @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"); | ||
| } |
There was a problem hiding this comment.
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.
| public static PayoutOrder aCompletedOrder() { | ||
| return aPayoutProcessingOrder().completePayout(PARTNER_REFERENCE, java.time.Instant.now()); | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
Summary
canApply()queries, immutability assertionsPayoutOrderFixturesin testFixtures with state factories for all 10 PayoutOrder statesTest Breakdown
Test plan
./gradlew :fiat-off-ramp:fiat-off-ramp:test)Closes STA-148
🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes