diff --git a/apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/delegates/CashLinkDelegateTest.kt b/apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/delegates/CashLinkDelegateTest.kt index 3412720005..75c16ef281 100644 --- a/apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/delegates/CashLinkDelegateTest.kt +++ b/apps/flipcash/shared/session/src/test/kotlin/com/flipcash/app/session/internal/delegates/CashLinkDelegateTest.kt @@ -11,7 +11,9 @@ import com.flipcash.app.session.SettledClaim import com.flipcash.app.session.internal.SessionStateHolder import com.flipcash.app.tokens.TokenCoordinator import com.flipcash.services.user.UserManager +import com.flipcash.core.R import com.getcode.manager.BottomBarManager +import com.getcode.opencode.internal.transactors.ReceiveGiftTransactorError import com.getcode.opencode.model.accounts.AccountCluster import com.getcode.opencode.model.financial.LocalFiat import com.getcode.opencode.model.financial.CurrencyCode @@ -267,6 +269,28 @@ class CashLinkDelegateTest { ) } + @Test + fun `a claim rejected as already claimed shows the already collected alert`() = runTest { + every { resources.getString(R.string.error_title_alreadyCollected) } returns "Already Collected" + val delegate = createDelegate() + + val onError = slot<(Throwable) -> Unit>() + delegate.openCashLink("validEntropy123") + verify { + billController.receiveGiftCard( + entropy = any(), + owner = any(), + claimIfOwned = any(), + onReceived = any(), + onError = capture(onError), + ) + } + onError.captured.invoke(ReceiveGiftTransactorError.AlreadyClaimed()) + runCurrent() + + assertEquals("Already Collected", BottomBarManager.messages.value.single().title) + } + @Test fun `openCashLink clears bottom bar before processing`() = runTest { // Add a message to BottomBarManager first diff --git a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt index f2ece09f20..203b3f9ebe 100644 --- a/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt +++ b/services/opencode/src/main/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactor.kt @@ -154,10 +154,15 @@ internal class ReceiveGiftCardTransactor( }, onFailure = { error -> onStep("intent") - if (error is SubmitIntentError.StaleState && error.isGiftCardAlreadyClaimed) { - Result.failure(error) - } else { - logAndFail(error) + // The account query can report NOT_CLAIMED after another claim + // has been accepted, so the pre-claim checks pass and the server + // rejects at submit. Map those to the same errors the checks use. + when { + error is SubmitIntentError.StaleState && error.isGiftCardAlreadyClaimed -> + Result.failure(ReceiveGiftTransactorError.AlreadyClaimed()) + error is SubmitIntentError.StaleState && error.isGiftCardExpired -> + Result.failure(ReceiveGiftTransactorError.Expired()) + else -> logAndFail(error) } } ) diff --git a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactorTest.kt b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactorTest.kt index 416dc32d9e..3afac5e61c 100644 --- a/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactorTest.kt +++ b/services/opencode/src/test/kotlin/com/getcode/opencode/internal/transactors/ReceiveGiftCardTransactorTest.kt @@ -21,6 +21,7 @@ import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test +import kotlin.test.assertIs import kotlin.test.assertSame import kotlin.test.assertTrue @@ -70,20 +71,47 @@ class ReceiveGiftCardTransactorTest { // region intent submission @Test - fun `start fails with the stale state when the gift card was already claimed`() = runTest { + fun `start fails with AlreadyClaimed when the intent is rejected as already claimed`() = runTest { + // The account query can still report NOT_CLAIMED when another claim has + // just been accepted, so the pre-claim check passes and submit is rejected. setupWithOwner() setupClaimableGiftCard() - val alreadyClaimed = SubmitIntentError.StaleState( - listOf("gift card balance has already been claimed") + coEvery { + transactionController.receiveRemotely(any(), any(), any(), any()) + } returns Result.failure( + SubmitIntentError.StaleState(listOf("gift card balance has already been claimed")) ) + + val result = transactor.start(claimIfOwned = false) + + assertIs(result.exceptionOrNull()) + } + + @Test + fun `start fails with Expired when the intent is rejected as expired`() = runTest { + setupWithOwner() + setupClaimableGiftCard() coEvery { transactionController.receiveRemotely(any(), any(), any(), any()) - } returns Result.failure(alreadyClaimed) + } returns Result.failure(SubmitIntentError.StaleState(listOf("gift card is expired"))) val result = transactor.start(claimIfOwned = false) - assertTrue(result.isFailure) - assertSame(alreadyClaimed, result.exceptionOrNull()) + assertIs(result.exceptionOrNull()) + } + + @Test + fun `start passes through other stale state rejections`() = runTest { + setupWithOwner() + setupClaimableGiftCard() + val raced = SubmitIntentError.StaleState(listOf("race detected: nonce")) + coEvery { + transactionController.receiveRemotely(any(), any(), any(), any()) + } returns Result.failure(raced) + + val result = transactor.start(claimIfOwned = false) + + assertSame(raced, result.exceptionOrNull()) } @Test