Files
kvitt/android/data/build.gradle.kts
T
jacobh2 b09f3220a1 M12c: category contract v2 on Android (14 defaults, suggestCategory)
- :core:model: DefaultCategoryKey in AC-7.2 order (14 keys: housing, insurance, children, fees added),
  ColorToken + SLATE, AMBER, CategoryIcons.ALL + key, shield, account_balance (33),
  ExpenseRepository.suggestCategory(accountId, description); ContractTest updated.
- :domain: parseCategoryKeywords (pure, shared by :data and :core:fakes) with a parser test; the
  suggestCategory records and rule unchanged, suggest_category vectors pass.
- :data: PlatformUnicodeText (java.text.Normalizer, Character), CategoryKeywords parsed once from the
  Java resource that copyCategoryKeywords copies from contracts/category-keywords.csv at build time
  (missing file fails the build), ExpenseRepositoryImpl.suggestCategory over the Room snapshot and the
  DataStore last-used category; tests: byte identity and the 14.8 rules of the bundled table, the
  UnicodeText binding, and Room/Robolectric cases for history, first token, ties, keywords, fallback,
  archived categories, deleted expenses, both users and accounts, null account.
- :core:fakes: the 14 defaults with the schema v2 ids, fake suggestCategory through :domain with its own
  build-time copy of the table; tests.
- Room schema unchanged (version 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-18 08:21:59 +02:00

88 lines
3.9 KiB
Kotlin

// :data — Room database, Ktor client, token store, sync engine, WorkManager workers, the real repositories and the APK
// updater (SPEC §6.2, §14.2, §14.4; owned by M5). Hilt modules here provide the implementations; :app (M8) binds the
// :core:model interfaces to them.
plugins {
id("kvitt.android.library")
id("kvitt.android.hilt")
alias(libs.plugins.ksp)
alias(libs.plugins.room3)
alias(libs.plugins.kotlin.serialization)
}
room3 {
schemaDirectory("$projectDir/schemas")
}
dependencies {
implementation(project(":core:model"))
implementation(project(":domain"))
implementation(libs.androidx.room3.runtime)
ksp(libs.androidx.room3.compiler)
implementation(libs.androidx.sqlite.bundled)
implementation(libs.androidx.datastore.preferences)
implementation(libs.androidx.work.runtime.ktx)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.okhttp)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.datetime)
implementation(libs.coil)
implementation(libs.coil.network.ktor3)
testImplementation(libs.ktor.client.mock)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.robolectric)
}
// Unit tests read the shared examples (SPEC §14.3.5) and the exported Room schemas (§14.2.3) in place; both are inputs,
// so a changed contract file re-runs the tests.
val contractsExamples = rootProject.layout.projectDirectory.dir("../contracts/examples")
val roomSchemas = layout.projectDirectory.dir("schemas")
val categoryKeywordsTable = rootProject.layout.projectDirectory.file("../contracts/category-keywords.csv")
tasks.withType<Test>().configureEach {
inputs.dir(contractsExamples).withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("contractsExamples")
inputs.dir(roomSchemas).withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("roomSchemas")
systemProperty("kvitt.contracts.examples", contractsExamples.asFile.absolutePath)
systemProperty("kvitt.room.schemas", roomSchemas.asFile.absolutePath)
inputs.file(categoryKeywordsTable).withPathSensitivity(PathSensitivity.RELATIVE).withPropertyName("categoryKeywords")
systemProperty("kvitt.contracts.keywords", categoryKeywordsTable.asFile.absolutePath)
}
// The keyword table of the category suggestion (UX-3, SPEC §9.11 CS-6) is bundled as the Java resource
// se/hagstedt/kvitt/data/category-keywords.csv, copied from contracts/category-keywords.csv at build time (never
// hand-copied). A missing table fails the build: the task's input file must exist.
abstract class CopyCategoryKeywords : DefaultTask() {
@get:InputFile
@get:PathSensitive(PathSensitivity.NONE)
abstract val table: RegularFileProperty
@get:Input
abstract val resourcePath: Property<String>
@get:OutputDirectory
abstract val outputDir: DirectoryProperty
@TaskAction
fun copy() {
val source = table.get().asFile
if (!source.isFile) throw GradleException("missing ${source.path}")
val root = outputDir.get().asFile
root.deleteRecursively()
val target = root.resolve(resourcePath.get())
target.parentFile.mkdirs()
source.copyTo(target)
}
}
val copyCategoryKeywords = tasks.register<CopyCategoryKeywords>("copyCategoryKeywords") {
description = "Copies contracts/category-keywords.csv into the main Java resources."
table.set(categoryKeywordsTable)
resourcePath.set("se/hagstedt/kvitt/data/category-keywords.csv")
outputDir.set(layout.buildDirectory.dir("generated/category-keywords"))
}
androidComponents {
onVariants { variant -> variant.sources.resources?.addGeneratedSourceDirectory(copyCategoryKeywords, CopyCategoryKeywords::outputDir) }
}