diff --git a/examples/folio/app/shared/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionViewModel.kt b/examples/folio/app/shared/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionViewModel.kt index fbb160d..20337c2 100644 --- a/examples/folio/app/shared/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionViewModel.kt +++ b/examples/folio/app/shared/src/commonMain/kotlin/app/folio/feature/ledger/AddTransactionViewModel.kt @@ -3,6 +3,7 @@ package app.folio.feature.ledger import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import app.folio.core.data.Account +import app.folio.core.data.MAX_TRANSACTION_AMOUNT_CENTS import app.folio.core.data.Repository import app.folio.core.data.TxnType import app.folio.navigation.Navigator @@ -87,6 +88,10 @@ class AddTransactionViewModel( form.update { it.copy(error = "Amount must be greater than zero") } return } + if (cents > MAX_TRANSACTION_AMOUNT_CENTS) { + form.update { it.copy(error = "Amount is too large (max \$1,000,000.00)") } + return + } viewModelScope.launch { try { repository.createTransaction(accountId, s.type, cents, s.note) diff --git a/examples/folio/core/src/commonMain/kotlin/app/folio/core/data/Repository.kt b/examples/folio/core/src/commonMain/kotlin/app/folio/core/data/Repository.kt index 020407e..ea8d8e0 100644 --- a/examples/folio/core/src/commonMain/kotlin/app/folio/core/data/Repository.kt +++ b/examples/folio/core/src/commonMain/kotlin/app/folio/core/data/Repository.kt @@ -6,6 +6,8 @@ import dev.zacsweers.metro.Inject import dev.zacsweers.metro.SingleIn import kotlinx.coroutines.flow.StateFlow +const val MAX_TRANSACTION_AMOUNT_CENTS = 100_000_000L + @SingleIn(AppScope::class) @Inject class Repository(private val store: LedgerStore) { @@ -27,6 +29,7 @@ class Repository(private val store: LedgerStore) { suspend fun createTransaction(accountId: String, type: TxnType, amount: Long, note: String): Transaction { require(amount > 0) { "Amount must be greater than zero" } + require(amount <= MAX_TRANSACTION_AMOUNT_CENTS) { "Amount is too large (max \$1,000,000.00)" } requireNotNull(getAccount(accountId)) { "Account not found" } val txn = Transaction( id = Platform.makeId(), diff --git a/examples/folio/core/src/commonTest/kotlin/app/folio/core/data/RepositoryTest.kt b/examples/folio/core/src/commonTest/kotlin/app/folio/core/data/RepositoryTest.kt new file mode 100644 index 0000000..65f9d81 --- /dev/null +++ b/examples/folio/core/src/commonTest/kotlin/app/folio/core/data/RepositoryTest.kt @@ -0,0 +1,66 @@ +package app.folio.core.data + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertTrue + +private class FakeLedgerStore : LedgerStore { + override val accounts = MutableStateFlow>(emptyList()) + override val transactions = MutableStateFlow>(emptyList()) + override val session = MutableStateFlow(null) + + override suspend fun accountExistsByName(name: String): Boolean = + accounts.value.any { it.name == name } + + override suspend fun insertAccount(id: String, name: String, createdAt: Long) { + accounts.value = accounts.value + Account(id, name, createdAt) + } + + override suspend fun insertTxn( + id: String, + accountId: String, + type: TxnType, + amount: Long, + note: String, + createdAt: Long, + ) { + transactions.value = transactions.value + Transaction(id, accountId, type, amount, note, createdAt) + } + + override suspend fun upsertSession(user: String, loggedInAt: Long) { + session.value = Session(user, loggedInAt) + } + + override suspend fun clearSession() { + session.value = null + } +} + +class RepositoryTest { + @Test + fun rejectsAmountAboveOneMillionDollars() = runTest { + val repository = Repository(FakeLedgerStore()) + val account = repository.createAccount("Checking") + + assertFailsWith { + repository.createTransaction(account.id, TxnType.credit, 100_000_001L, "") + } + assertFailsWith { + repository.createTransaction(account.id, TxnType.credit, Long.MAX_VALUE, "") + } + assertTrue(repository.transactions.value.isEmpty()) + } + + @Test + fun acceptsAmountAtOneMillionDollars() = runTest { + val repository = Repository(FakeLedgerStore()) + val account = repository.createAccount("Checking") + + repository.createTransaction(account.id, TxnType.credit, 100_000_000L, "rent") + + assertEquals(listOf(100_000_000L), repository.transactions.value.map { it.amount }) + } +}