M2b: Android skeleton, :core:model contract, :domain passing all vectors
Gradle project in android/ with every module of SPEC §6.2 (build-logic convention plugins, version catalog, architecture check in check), :core:model types and repository interfaces of §14.2.4 and §14.5, :domain implementing §14.5.2 and §14.5.4 (125 vector cases), scenario-driven fakes with the empty scenario, Room 3 entities and KvittDatabase v1 with exported schema, :app shell with Hilt, en/sv strings with MissingTranslation as a lint error, Roborazzi goldens of the empty screen in light and dark. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Accounts (S-5, S-6), SPEC §14.2.4.
|
||||
|
||||
data class AccountDetailState(val account: Account, val balance: BalanceView, val months: List<LedgerMonth>, val deletable: Boolean, val users: List<User>)
|
||||
|
||||
interface AccountRepository {
|
||||
fun detail(id: AccountId): Flow<AccountDetailState?>
|
||||
fun all(): Flow<List<Account>>
|
||||
suspend fun create(name: String, defaultShares: Map<UserId, Int>): OnlineResult<AccountId>
|
||||
suspend fun update(id: AccountId, name: String, defaultShares: Map<UserId, Int>): OnlineResult<Unit>
|
||||
suspend fun setArchived(id: AccountId, archived: Boolean): OnlineResult<Unit>
|
||||
suspend fun delete(id: AccountId): OnlineResult<Unit>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Analytics (S-11, S-12), SPEC §14.2.4.
|
||||
|
||||
data class AnalyticsFilter(val preset: RangePreset, val customStart: String?, val customEnd: String?, val granularity: Granularity, val measure: Measure, val groupBy: GroupBy)
|
||||
|
||||
/** label null: UI uses the default category name. */
|
||||
data class GroupKey(val id: String, val label: String?, val color: ColorToken?)
|
||||
|
||||
data class SpendBucket(val key: String, val start: String, val end: String, val totals: Map<String, Long>, val totalOre: Long)
|
||||
|
||||
data class AnalyticsState(val filter: AnalyticsFilter, val start: String, val end: String, val groups: List<GroupKey>, val buckets: List<SpendBucket>, val byCategory: List<Pair<GroupKey, Long>>, val byAccount: List<Pair<GroupKey, Long>>, val totalOre: Long)
|
||||
|
||||
data class DrillDownState(val title: String, val totalOre: Long, val rows: List<LedgerRow.ExpenseRow>)
|
||||
|
||||
interface AnalyticsRepository {
|
||||
fun analytics(filter: AnalyticsFilter): Flow<AnalyticsState>
|
||||
fun drillDown(filter: AnalyticsFilter, bucketKey: String?, categoryId: CategoryId?, accountId: AccountId?): Flow<DrillDownState>
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
// Enums (SPEC §14.2.4). Wire value of every enum = lowercase name (SplitMode.DEFAULT <-> "default").
|
||||
|
||||
enum class Role { ADMIN, MEMBER }
|
||||
|
||||
enum class SplitMode { DEFAULT, RATIO, EXACT, FULL }
|
||||
|
||||
enum class InviteKind { ADMIN_SETUP, PARTNER, DEVICE }
|
||||
|
||||
enum class EntityType { EXPENSE, SETTLEMENT }
|
||||
|
||||
enum class AuditAction { CREATE, UPDATE, DELETE }
|
||||
|
||||
enum class AuditSource { APP, RECURRING, IMPORT }
|
||||
|
||||
enum class DefaultCategoryKey { GROCERIES, RESTAURANTS, HOME, UTILITIES, TRANSPORT, TRAVEL, ENTERTAINMENT, HEALTH, SHOPPING, OTHER }
|
||||
|
||||
enum class ColorToken { GREEN, ORANGE, BROWN, YELLOW, BLUE, TEAL, PURPLE, RED, PINK, GREY, INDIGO, LIME }
|
||||
|
||||
enum class RangePreset { THIS_MONTH, LAST_3_MONTHS, LAST_6_MONTHS, LAST_12_MONTHS, THIS_YEAR, ALL_TIME, CUSTOM }
|
||||
|
||||
enum class Granularity { MONTH, WEEK }
|
||||
|
||||
enum class Measure { HOUSEHOLD, MY_SHARE }
|
||||
|
||||
enum class GroupBy { CATEGORY, ACCOUNT }
|
||||
|
||||
enum class SyncRejectionReason { STALE, DELETED, VALIDATION_FAILED, REFERENCE_NOT_FOUND, CLOCK_SKEW }
|
||||
|
||||
/** Category icon tokens: Material Symbols names, the exhaustive v1 set of SPEC §9.7, in that order. */
|
||||
object CategoryIcons {
|
||||
val ALL: List<String> = listOf(
|
||||
"shopping_cart", "restaurant", "local_cafe", "home", "bolt", "water_drop", "wifi", "directions_car", "train",
|
||||
"local_gas_station", "flight", "hotel", "luggage", "movie", "music_note", "sports_esports", "fitness_center",
|
||||
"medical_services", "medication", "shopping_bag", "checkroom", "pets", "child_care", "school", "card_giftcard",
|
||||
"celebration", "build", "chair", "subscriptions", "category",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Expenses (S-7, S-8), SPEC §14.2.4.
|
||||
|
||||
data class ExpenseDraft(val id: ExpenseId?, val accountId: AccountId?, val categoryId: CategoryId?, val amountOre: Long?, val description: String, val notes: String, val date: String, val payer: UserId, val mode: SplitMode, val ratioBp: Map<UserId, Int>?, val exactOre: Map<UserId, Long>?, val fullOn: UserId?)
|
||||
|
||||
data class ExpenseEditorOptions(val me: User, val partner: User, val accounts: List<Account>, val categories: List<Category>, val defaultAccountId: AccountId?, val defaultCategoryFor: Map<AccountId, CategoryId>, val today: String)
|
||||
|
||||
data class TimelineLine(val actor: User?, val action: AuditAction, val source: AuditSource, val clientAt: String, val changes: List<FieldChange>, val pendingLocal: Boolean)
|
||||
|
||||
/** Ids already resolved to names; amounts and dates raw (öre as String, ISO date), formatted by the UI. */
|
||||
data class FieldChange(val field: String, val oldDisplay: String?, val newDisplay: String?)
|
||||
|
||||
data class ExpenseDetailState(val id: ExpenseId, val account: Account, val category: Category, val amountOre: Long, val description: String, val notes: String, val date: String, val payer: User, val mode: SplitMode, val shares: List<Share>, val recurringTemplateName: String?, val deleted: Boolean, val pending: Boolean, val timeline: List<TimelineLine>)
|
||||
|
||||
interface ExpenseRepository {
|
||||
fun editorOptions(): Flow<ExpenseEditorOptions>
|
||||
suspend fun load(id: ExpenseId): ExpenseDraft?
|
||||
fun preview(draft: ExpenseDraft): List<Share>? // null while the draft cannot be computed
|
||||
suspend fun save(draft: ExpenseDraft): SaveResult<ExpenseId>
|
||||
suspend fun delete(id: ExpenseId)
|
||||
fun detail(id: ExpenseId): Flow<ExpenseDetailState?>
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Balances shared by several screens and Home (S-4), SPEC §14.2.4.
|
||||
|
||||
/** netOre is the viewer's net (§9.3): > 0 partner owes viewer. */
|
||||
data class BalanceView(val netOre: Long, val partner: User)
|
||||
|
||||
data class AccountSummary(val account: Account, val netOre: Long, val expensesThisMonth: Int)
|
||||
|
||||
data class SyncSummary(val lastSuccessAt: String?, val pendingCount: Int, val rejectedCount: Int, val clockSkew: Boolean, val running: Boolean)
|
||||
|
||||
data class HomeState(val me: User, val partner: User?, val global: BalanceView?, val active: List<AccountSummary>, val archived: List<AccountSummary>, val sync: SyncSummary, val update: UpdateInfo?)
|
||||
|
||||
interface HomeRepository {
|
||||
fun home(): Flow<HomeState>
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
// Ids (SPEC §14.2.4). Values are UUIDv7 strings on the wire.
|
||||
|
||||
@JvmInline value class UserId(val value: String)
|
||||
|
||||
@JvmInline value class AccountId(val value: String)
|
||||
|
||||
@JvmInline value class CategoryId(val value: String)
|
||||
|
||||
@JvmInline value class ExpenseId(val value: String)
|
||||
|
||||
@JvmInline value class SettlementId(val value: String)
|
||||
|
||||
@JvmInline value class TemplateId(val value: String)
|
||||
|
||||
@JvmInline value class DeviceId(val value: String)
|
||||
|
||||
@JvmInline value class InviteId(val value: String)
|
||||
@@ -0,0 +1,17 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
// Ledger rows (S-5, S-12), SPEC §14.2.4.
|
||||
|
||||
sealed interface ExpenseEffect {
|
||||
data class Lent(val ore: Long) : ExpenseEffect
|
||||
data class Borrowed(val ore: Long) : ExpenseEffect
|
||||
data object NotInvolved : ExpenseEffect
|
||||
data object PaidForYourself : ExpenseEffect
|
||||
}
|
||||
|
||||
sealed interface LedgerRow {
|
||||
data class ExpenseRow(val id: ExpenseId, val date: String, val description: String, val category: Category, val amountOre: Long, val effect: ExpenseEffect, val pending: Boolean) : LedgerRow
|
||||
data class SettlementRow(val id: SettlementId, val date: String, val from: User, val to: User, val amountOre: Long, val accountCount: Int, val pending: Boolean) : LedgerRow
|
||||
}
|
||||
|
||||
data class LedgerMonth(val period: String, val householdSpendOre: Long, val rows: List<LedgerRow>)
|
||||
@@ -0,0 +1,25 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
// Plain records and results (SPEC §14.2.4).
|
||||
|
||||
data class User(val id: UserId, val role: Role, val displayName: String, val pictureVersion: Int?)
|
||||
|
||||
data class Account(val id: AccountId, val name: String, val defaultShares: Map<UserId, Int>, val createdAt: String, val archived: Boolean)
|
||||
|
||||
data class Category(val id: CategoryId, val defaultKey: DefaultCategoryKey?, val customName: String?, val icon: String, val color: ColorToken, val sortOrder: Int, val archived: Boolean)
|
||||
|
||||
data class Share(val userId: UserId, val shareOre: Long, val shareBp: Int?)
|
||||
|
||||
sealed interface OnlineResult<out T> {
|
||||
data class Ok<T>(val value: T) : OnlineResult<T>
|
||||
data object Offline : OnlineResult<Nothing>
|
||||
data class Failed(val code: String, val field: String?) : OnlineResult<Nothing> // code from §14.3.2
|
||||
}
|
||||
|
||||
sealed interface SaveResult<out T> {
|
||||
data class Saved<T>(val id: T) : SaveResult<T>
|
||||
data class Invalid(val errors: List<FieldError>) : SaveResult<Nothing>
|
||||
}
|
||||
|
||||
/** `error` is a domain error token of §14.5.3 or "required". */
|
||||
data class FieldError(val field: String, val error: String)
|
||||
@@ -0,0 +1,18 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Session and onboarding (S-1, S-2, S-3, S-18), SPEC §14.2.4.
|
||||
|
||||
sealed interface SessionState {
|
||||
data object Unpaired : SessionState
|
||||
data class Paired(val me: User, val partner: User?, val deviceId: DeviceId) : SessionState
|
||||
data object Revoked : SessionState
|
||||
data object Unrecognised : SessionState // 401 unauthorized: pair again, data kept (§14.4.8)
|
||||
}
|
||||
|
||||
interface SessionRepository {
|
||||
val session: Flow<SessionState>
|
||||
suspend fun pair(serverUrl: String, code: String, displayName: String?, deviceName: String): OnlineResult<User>
|
||||
suspend fun acknowledgeRevoked() // wipes local data (AC-3.3) and moves to Unpaired
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Settings (S-13 to S-15, S-19 to S-22), SPEC §14.2.4.
|
||||
|
||||
interface ProfileRepository {
|
||||
fun household(): Flow<List<User>>
|
||||
suspend fun setDisplayName(name: String): OnlineResult<Unit>
|
||||
suspend fun setPicture(bytes: ByteArray, mimeType: String): OnlineResult<Unit>
|
||||
suspend fun removePicture(): OnlineResult<Unit>
|
||||
}
|
||||
|
||||
interface CategoryRepository {
|
||||
fun all(): Flow<List<Category>>
|
||||
suspend fun create(name: String, icon: String, color: ColorToken): OnlineResult<CategoryId>
|
||||
suspend fun update(id: CategoryId, name: String?, icon: String, color: ColorToken): OnlineResult<Unit> // name null = reset default name
|
||||
suspend fun reorder(idsInOrder: List<CategoryId>): OnlineResult<Unit>
|
||||
suspend fun setArchived(id: CategoryId, archived: Boolean): OnlineResult<Unit>
|
||||
suspend fun delete(id: CategoryId): OnlineResult<Unit>
|
||||
}
|
||||
|
||||
data class RecurringTemplate(val id: TemplateId, val draft: ExpenseDraft, val dayOfMonth: Int, val startPeriod: String, val endPeriod: String?, val paused: Boolean, val nextOccurrence: String?)
|
||||
|
||||
interface RecurringRepository {
|
||||
fun all(): Flow<List<RecurringTemplate>>
|
||||
suspend fun create(t: RecurringTemplate): OnlineResult<TemplateId>
|
||||
suspend fun update(t: RecurringTemplate): OnlineResult<Unit>
|
||||
suspend fun delete(id: TemplateId): OnlineResult<Unit>
|
||||
}
|
||||
|
||||
data class Device(val id: DeviceId, val user: User, val name: String, val pairedAt: String, val lastSeenAt: String, val appVersionCode: Int?, val current: Boolean, val revoked: Boolean)
|
||||
|
||||
interface DeviceRepository {
|
||||
suspend fun list(): OnlineResult<List<Device>>
|
||||
suspend fun revoke(id: DeviceId): OnlineResult<Unit>
|
||||
}
|
||||
|
||||
/** code and link only right after creation. */
|
||||
data class Invite(val id: InviteId, val kind: InviteKind, val targetUser: UserId?, val code: String?, val link: String?, val createdAt: String, val expiresAt: String)
|
||||
|
||||
interface InviteRepository {
|
||||
suspend fun list(): OnlineResult<List<Invite>>
|
||||
suspend fun create(kind: InviteKind, targetUser: UserId?): OnlineResult<Invite>
|
||||
suspend fun cancel(id: InviteId): OnlineResult<Unit>
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Settlements (S-9, S-10), SPEC §14.2.4.
|
||||
|
||||
data class AllocationLine(val account: Account, val payerNetOre: Long, val amountOre: Long, val overSettles: Boolean)
|
||||
|
||||
data class SettleState(val payer: User, val recipient: User, val amountOre: Long?, val lines: List<AllocationLine>, val remainingOre: Long, val canConfirm: Boolean, val date: String, val note: String)
|
||||
|
||||
data class SettleRequest(val editing: SettlementId?, val payer: UserId, val amountOre: Long?, val fixedAccount: AccountId?, val allocations: Map<AccountId, Long>?, val date: String, val note: String)
|
||||
|
||||
data class SettlementDetailState(val id: SettlementId, val from: User, val to: User, val amountOre: Long, val date: String, val note: String, val allocations: List<Pair<Account, Long>>, val deleted: Boolean, val pending: Boolean, val timeline: List<TimelineLine>)
|
||||
|
||||
interface SettlementRepository {
|
||||
suspend fun start(editing: SettlementId?, fixedAccount: AccountId?): SettleState // defaults per AC-11.3 / AC-11.4
|
||||
suspend fun recompute(request: SettleRequest): SettleState // allocations == null: propose (§9.4); else validate and warn
|
||||
suspend fun save(request: SettleRequest): SaveResult<SettlementId>
|
||||
suspend fun delete(id: SettlementId)
|
||||
fun detail(id: SettlementId): Flow<SettlementDetailState?>
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
// Sync status, update, about and connectivity (S-16, S-17, S-22, §9.9), SPEC §14.2.4.
|
||||
|
||||
data class Rejection(val mutationId: String, val entityType: EntityType, val entityId: String, val reason: SyncRejectionReason, val summary: String, val rejectedAt: String)
|
||||
|
||||
data class SyncStatusState(val summary: SyncSummary, val pending: List<LedgerRow>, val rejections: List<Rejection>)
|
||||
|
||||
interface SyncRepository {
|
||||
fun status(): Flow<SyncStatusState>
|
||||
suspend fun syncNow()
|
||||
suspend fun dismiss(mutationId: String)
|
||||
}
|
||||
|
||||
data class UpdateInfo(val versionCode: Int, val versionName: String, val sizeBytes: Long)
|
||||
|
||||
sealed interface UpdateState {
|
||||
data object None : UpdateState
|
||||
data class Available(val info: UpdateInfo) : UpdateState
|
||||
data class Downloading(val info: UpdateInfo, val progress: Float) : UpdateState
|
||||
data class Failed(val info: UpdateInfo) : UpdateState
|
||||
data class NeedsInstallPermission(val info: UpdateInfo) : UpdateState
|
||||
}
|
||||
|
||||
interface UpdateRepository {
|
||||
val state: Flow<UpdateState>
|
||||
suspend fun check()
|
||||
suspend fun install()
|
||||
}
|
||||
|
||||
data class AboutState(val versionName: String, val versionCode: Int, val serverUrl: String, val serverVersion: String?)
|
||||
|
||||
interface AboutRepository {
|
||||
fun about(): Flow<AboutState>
|
||||
}
|
||||
|
||||
/** §9.9 */
|
||||
interface ConnectivityRepository {
|
||||
val online: Flow<Boolean>
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package se.hagstedt.kvitt.model.analytics
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
|
||||
// Analytics input and output types (SPEC §14.5.4), package per the orchestrator's M2b decision.
|
||||
|
||||
data class AnalyticsExpense(val date: LocalDate, val accountId: String, val categoryId: String, val amountOre: Long, val shares: Map<String, Long>)
|
||||
|
||||
data class DateRange(val start: LocalDate, val endInclusive: LocalDate)
|
||||
|
||||
enum class AnalyticsGranularity { MONTH, WEEK }
|
||||
|
||||
sealed interface AnalyticsMeasure {
|
||||
data object Household : AnalyticsMeasure
|
||||
data class Share(val userId: String) : AnalyticsMeasure
|
||||
}
|
||||
|
||||
enum class AnalyticsGroupBy { CATEGORY, ACCOUNT }
|
||||
|
||||
data class Bucket(val key: String, val start: LocalDate, val endInclusive: LocalDate, val totals: Map<String, Long>, val total: Long)
|
||||
|
||||
data class SpendSeries(val buckets: List<Bucket>, val groupTotals: Map<String, Long>, val grandTotal: Long)
|
||||
@@ -0,0 +1,16 @@
|
||||
package se.hagstedt.kvitt.model.money
|
||||
|
||||
// SPEC §14.5.1
|
||||
|
||||
data class AccountPosition(val accountId: String, val createdAt: String, val archived: Boolean, val payerNetOre: Long)
|
||||
|
||||
enum class AllocationError { AMOUNT_OUT_OF_RANGE, NO_ACCOUNTS }
|
||||
|
||||
data class Allocation(val accountId: String, val amountOre: Long)
|
||||
|
||||
sealed interface AllocationResult {
|
||||
data class Ok(val allocations: List<Allocation>, val excessOre: Long, val overSettledAccountIds: List<String>) : AllocationResult
|
||||
data class Err(val error: AllocationError) : AllocationResult
|
||||
}
|
||||
|
||||
enum class SettlementError { AMOUNT_OUT_OF_RANGE, SAME_USER, USER_UNKNOWN, NO_ALLOCATIONS, TOO_MANY_ALLOCATIONS, ALLOCATION_NOT_POSITIVE, DUPLICATE_ACCOUNT, ALLOCATION_SUM_MISMATCH }
|
||||
@@ -0,0 +1,11 @@
|
||||
package se.hagstedt.kvitt.model.money
|
||||
|
||||
// SPEC §14.5.1
|
||||
|
||||
data class BalanceExpense(val accountId: String, val amountOre: Long, val payerUserId: String, val shares: Map<String, Long>)
|
||||
|
||||
/** allocations: accountId -> öre. */
|
||||
data class BalanceSettlement(val fromUserId: String, val toUserId: String, val allocations: Map<String, Long>)
|
||||
|
||||
/** perAccount: accountId -> userId -> net; global: userId -> net. */
|
||||
data class Balances(val perAccount: Map<String, Map<String, Long>>, val global: Map<String, Long>)
|
||||
@@ -0,0 +1,17 @@
|
||||
package se.hagstedt.kvitt.model.money
|
||||
|
||||
// Domain input and output types (SPEC §14.5.1). Ids are plain strings; money is öre (Long); bp is Int.
|
||||
|
||||
sealed interface SplitSpec {
|
||||
data class Ratio(val bp: Map<String, Int>) : SplitSpec // modes default and ratio
|
||||
data class Exact(val ore: Map<String, Long>) : SplitSpec // mode exact
|
||||
data class Full(val bearerUserId: String) : SplitSpec // mode full
|
||||
}
|
||||
|
||||
/** Checked in this order; the first failure is returned (§14.5.2). Wire token = lowercase name (§14.5.3). */
|
||||
enum class SplitError { AMOUNT_OUT_OF_RANGE, USERS_INVALID, PAYER_UNKNOWN, BP_OUT_OF_RANGE, BP_SUM_NOT_10000, EXACT_NEGATIVE, EXACT_SUM_MISMATCH, BEARER_UNKNOWN, SHARE_USERS_MISMATCH }
|
||||
|
||||
sealed interface SplitResult {
|
||||
data class Ok(val shares: Map<String, Long>) : SplitResult
|
||||
data class Err(val error: SplitError) : SplitResult
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package se.hagstedt.kvitt.model
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import se.hagstedt.kvitt.model.money.AllocationError
|
||||
import se.hagstedt.kvitt.model.money.SettlementError
|
||||
import se.hagstedt.kvitt.model.money.SplitError
|
||||
|
||||
/** Wire values (lowercase enum names, SPEC §14.2.4, §14.5.3) and the §9.7 token lists. */
|
||||
class ContractTest {
|
||||
private fun wire(values: Array<out Enum<*>>) = values.map { it.name.lowercase() }
|
||||
|
||||
@Test
|
||||
fun enumWireValuesMatchTheSpec() {
|
||||
assertEquals(listOf("admin", "member"), wire(Role.entries.toTypedArray()))
|
||||
assertEquals(listOf("default", "ratio", "exact", "full"), wire(SplitMode.entries.toTypedArray()))
|
||||
assertEquals(listOf("admin_setup", "partner", "device"), wire(InviteKind.entries.toTypedArray()))
|
||||
assertEquals(listOf("expense", "settlement"), wire(EntityType.entries.toTypedArray()))
|
||||
assertEquals(listOf("create", "update", "delete"), wire(AuditAction.entries.toTypedArray()))
|
||||
assertEquals(listOf("app", "recurring", "import"), wire(AuditSource.entries.toTypedArray()))
|
||||
assertEquals(
|
||||
listOf("groceries", "restaurants", "home", "utilities", "transport", "travel", "entertainment", "health", "shopping", "other"),
|
||||
wire(DefaultCategoryKey.entries.toTypedArray()),
|
||||
)
|
||||
assertEquals(
|
||||
listOf("green", "orange", "brown", "yellow", "blue", "teal", "purple", "red", "pink", "grey", "indigo", "lime"),
|
||||
wire(ColorToken.entries.toTypedArray()),
|
||||
)
|
||||
assertEquals(
|
||||
listOf("this_month", "last_3_months", "last_6_months", "last_12_months", "this_year", "all_time", "custom"),
|
||||
wire(RangePreset.entries.toTypedArray()),
|
||||
)
|
||||
assertEquals(listOf("month", "week"), wire(Granularity.entries.toTypedArray()))
|
||||
assertEquals(listOf("household", "my_share"), wire(Measure.entries.toTypedArray()))
|
||||
assertEquals(listOf("category", "account"), wire(GroupBy.entries.toTypedArray()))
|
||||
assertEquals(
|
||||
listOf("stale", "deleted", "validation_failed", "reference_not_found", "clock_skew"),
|
||||
wire(SyncRejectionReason.entries.toTypedArray()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun domainErrorTokensMatchSpec14_5_3() {
|
||||
assertEquals(
|
||||
listOf(
|
||||
"amount_out_of_range", "users_invalid", "payer_unknown", "bp_out_of_range", "bp_sum_not_10000",
|
||||
"exact_negative", "exact_sum_mismatch", "bearer_unknown", "share_users_mismatch",
|
||||
),
|
||||
wire(SplitError.entries.toTypedArray()),
|
||||
)
|
||||
assertEquals(listOf("amount_out_of_range", "no_accounts"), wire(AllocationError.entries.toTypedArray()))
|
||||
assertEquals(
|
||||
listOf(
|
||||
"amount_out_of_range", "same_user", "user_unknown", "no_allocations", "too_many_allocations",
|
||||
"allocation_not_positive", "duplicate_account", "allocation_sum_mismatch",
|
||||
),
|
||||
wire(SettlementError.entries.toTypedArray()),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun categoryIconsAreTheThirtyTokensOfSpec9_7InOrder() {
|
||||
assertEquals(30, CategoryIcons.ALL.size)
|
||||
assertEquals(30, CategoryIcons.ALL.toSet().size)
|
||||
assertEquals("shopping_cart", CategoryIcons.ALL.first())
|
||||
assertEquals("category", CategoryIcons.ALL.last())
|
||||
// The ten default categories (AC-7.2) use icons from the set.
|
||||
listOf("shopping_cart", "restaurant", "home", "bolt", "directions_car", "flight", "movie", "medical_services", "shopping_bag", "category")
|
||||
.forEach { assertEquals(it, CategoryIcons.ALL.single { icon -> icon == it }) }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user