Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-23 13:17:24 +05:00
parent 8196faae1c
commit 1157642722
6 changed files with 55 additions and 21 deletions

View file

@ -49,7 +49,7 @@ object OrderConflictRules {
private fun blocks(intent: OrderIntent, order: Order): Boolean {
if (!order.isActive) return false
return when (intent) {
OrderIntent.IssueCard -> order.type.isIssuing()
OrderIntent.IssueCard -> order.type.isIssuing
OrderIntent.Withdraw -> order.type == OrderType.WITHDRAW
is OrderIntent.Freeze -> sameProductInstance(order, intent.productInstanceId) &&
order.type.isFreezeOrReissue()
@ -65,15 +65,7 @@ object OrderConflictRules {
return order.productInstanceId == productInstanceId
}
private fun OrderType.isIssuing(): Boolean {
return this == OrderType.CARD_ISSUE_ADDITIONAL ||
this == OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC ||
this == OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2
}
private fun OrderType.isFreezeOrReissue(): Boolean {
return this == OrderType.CARD_FREEZE ||
this == OrderType.CARD_UNFREEZE ||
this == OrderType.CARD_REISSUE
return this.isFreezingUnfreezing || this.isReissuing
}
}

View file

@ -8,8 +8,16 @@ enum class OrderStatus {
;
/** An order is active while it is still being processed (NEW or PROCESSING). */
val isActive: Boolean get() = this == NEW || this == PROCESSING
val isActive: Boolean get() = activeStatuses.contains(this)
/** Terminal statuses (COMPLETED or CANCELED) — used to invalidate the local order hint. */
val isTerminal: Boolean get() = this == COMPLETED || this == CANCELED
val isTerminal: Boolean get() = terminalStatuses.contains(this)
companion object {
/** Statuses of an in-flight order (still being processed). */
val activeStatuses: Set<OrderStatus> = setOf(NEW, PROCESSING)
/** Statuses of a finished order — no further state changes are expected. */
val terminalStatuses: Set<OrderStatus> = setOf(COMPLETED, CANCELED)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.domain.pay.model
import com.tangem.domain.pay.model.OrderType.Companion.fromString
import com.tangem.domain.pay.model.OrderType.Companion.issueCardTypes
/**
* Order type used for findOrders filtering and order-conflict checks.
@ -10,8 +11,9 @@ import com.tangem.domain.pay.model.OrderType.Companion.fromString
*/
enum class OrderType(val wireValue: String) {
CARD_ISSUE_ADDITIONAL("CARD_ISSUE_ADDITIONAL"),
CARD_ISSUE_VIRTUAL_RAIN_KYC_V2("CARD_ISSUE_VIRTUAL_RAIN_KYC_V2"),
CARD_ISSUE_VIRTUAL_RAIN("CARD_ISSUE_VIRTUAL_RAIN"),
CARD_ISSUE_VIRTUAL_RAIN_KYC("CARD_ISSUE_VIRTUAL_RAIN_KYC"),
CARD_ISSUE_VIRTUAL_RAIN_KYC_V2("CARD_ISSUE_VIRTUAL_RAIN_KYC_V2"),
CARD_REISSUE("CARD_REISSUE"),
CARD_FREEZE("CARD_FREEZE"),
CARD_UNFREEZE("CARD_UNFREEZE"),
@ -19,7 +21,29 @@ enum class OrderType(val wireValue: String) {
UNKNOWN(""),
;
/** `true` for any card-issuance order type — see [issueCardTypes]. */
val isIssuing: Boolean get() = issueCardTypes.contains(this)
/** `true` for card freeze / unfreeze orders. */
val isFreezingUnfreezing: Boolean get() = this == CARD_FREEZE || this == CARD_UNFREEZE
/** `true` for card reissue orders. */
val isReissuing: Boolean get() = this == CARD_REISSUE
companion object {
/**
* All order types that represent issuing a card: the first virtual card (and its KYC
* variants) and an additional card. Used both to filter `findOrders` and to detect
* issue-card conflicts.
*/
val issueCardTypes = setOf(
CARD_ISSUE_ADDITIONAL,
CARD_ISSUE_VIRTUAL_RAIN,
CARD_ISSUE_VIRTUAL_RAIN_KYC,
CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
)
fun fromString(value: String?): OrderType {
if (value.isNullOrBlank()) return UNKNOWN
return entries.firstOrNull { it.wireValue == value || it.name == value } ?: UNKNOWN

View file

@ -6,6 +6,7 @@ import arrow.core.raise.catch
import arrow.core.raise.either
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.pay.model.Offer
import com.tangem.domain.pay.model.OrderStatus
import com.tangem.domain.pay.model.OrderType
import com.tangem.domain.pay.model.TangemPayOrderInfo
import com.tangem.domain.pay.repository.CustomerOffersRepository
@ -56,11 +57,8 @@ class IssueAdditionalCardUseCase(
customerOrderRepository
.findOrders(
userWalletId = userWalletId,
types = setOf(
offer.data.orderType,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
),
types = setOf(offer.data.orderType) + OrderType.issueCardTypes,
statuses = OrderStatus.activeStatuses,
)
.bind()
},

View file

@ -26,6 +26,15 @@ internal class OrderConflictRulesTest {
assertThat(resolution).isInstanceOf(ConflictResolution.Blocked::class.java)
}
@Test
fun `IssueCard is blocked by an active plain virtual-card issue order`() {
val active = listOf(order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN, status = OrderStatus.PROCESSING))
val resolution = OrderConflictRules.resolve(OrderIntent.IssueCard, active)
assertThat(resolution).isInstanceOf(ConflictResolution.Blocked::class.java)
}
@Test
fun `IssueCard is allowed when only withdraw is active`() {
val active = listOf(order(type = OrderType.WITHDRAW, status = OrderStatus.PROCESSING))

View file

@ -67,10 +67,11 @@ internal class IssueAdditionalCardUseCaseTest {
userWalletId,
types = setOf(
OrderType.CARD_ISSUE_ADDITIONAL,
OrderType.CARD_ISSUE_VIRTUAL_RAIN,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
),
statuses = emptySet(),
statuses = setOf(OrderStatus.NEW, OrderStatus.PROCESSING),
)
} returns listOf(existing).right()
@ -89,10 +90,11 @@ internal class IssueAdditionalCardUseCaseTest {
userWalletId,
types = setOf(
OrderType.CARD_ISSUE_ADDITIONAL,
OrderType.CARD_ISSUE_VIRTUAL_RAIN,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
),
statuses = emptySet(),
statuses = setOf(OrderStatus.NEW, OrderStatus.PROCESSING),
)
} returns emptyList<Order>().right()
coEvery {
@ -117,10 +119,11 @@ internal class IssueAdditionalCardUseCaseTest {
userWalletId,
types = setOf(
OrderType.CARD_ISSUE_ADDITIONAL,
OrderType.CARD_ISSUE_VIRTUAL_RAIN,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC,
OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2,
),
statuses = emptySet(),
statuses = setOf(OrderStatus.NEW, OrderStatus.PROCESSING),
)
} returns emptyList<Order>().right()
val newOrder = order(