M12d: frequentCategoryIds and CategoryRepository.deletable (UX-9, D-059)

ExpenseEditorOptions.frequentCategoryIds per active account (up to five active
categories by non-deleted expense count in the account, ties by sort_order) and
CategoryRepository.deletable(id) (AC-7.6), from Room queries in :data and the
same rule in :core:fakes. Room schema unchanged (version 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 08:34:22 +02:00
parent e6768121d6
commit 557af7e00a
11 changed files with 383 additions and 8 deletions
@@ -6,7 +6,22 @@ import kotlinx.coroutines.flow.Flow
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)
/**
* [frequentCategoryIds] (UX-9, D-059): for each active account, up to five active categories ordered by the number of
* non-deleted expenses in that account that use them (every expense on the phone: both users, pending, imported and
* recurring included), descending; ties by `sort_order` ascending. Categories with no use in the account are left out,
* so a list may be shorter or empty; the editor fills its row up from `sort_order` (design rule, not contract).
*/
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,
val frequentCategoryIds: Map<AccountId, List<CategoryId>>,
)
data class TimelineLine(val actor: User?, val action: AuditAction, val source: AuditSource, val clientAt: String, val changes: List<FieldChange>, val pendingLocal: Boolean)
@@ -18,6 +18,13 @@ interface CategoryRepository {
suspend fun reorder(idsInOrder: List<CategoryId>): OnlineResult<Unit>
suspend fun setArchived(id: CategoryId, archived: Boolean): OnlineResult<Unit>
suspend fun delete(id: CategoryId): OnlineResult<Unit>
/**
* UX-9 (AC-7.6): false for a default category (`defaultKey != null`), one used by a non-deleted expense or a
* non-deleted recurring template, and an unknown or deleted id. Computed from the phone's copy; the server stays the
* authority, so [delete] can still fail with `category_is_default` or `category_in_use`.
*/
fun deletable(id: CategoryId): Flow<Boolean>
}
data class RecurringTemplate(val id: TemplateId, val draft: ExpenseDraft, val dayOfMonth: Int, val startPeriod: String, val endPeriod: String?, val paused: Boolean, val nextOccurrence: String?)